Initial import of LogDarioSeq baseline
Establishes version control for the personal Logseq DB fork before starting the server-client architecture migration and domain features described in docs/agent-guide and the approved implementation plan. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
249
cli/lib/skill.ml
Normal file
249
cli/lib/skill.ml
Normal file
@@ -0,0 +1,249 @@
|
||||
type install_opts = { global : bool }
|
||||
type parsed = Parsed_show | Parsed_install of install_opts
|
||||
|
||||
type action =
|
||||
| Skill_show of { source_path : Cli_primitive.path option }
|
||||
| Skill_install of {
|
||||
global : bool;
|
||||
source_path : Cli_primitive.path option;
|
||||
destination_dir : Cli_primitive.path option;
|
||||
destination_file : Cli_primitive.path option;
|
||||
}
|
||||
|
||||
type install_target = {
|
||||
scope : Cli_primitive.keyword;
|
||||
path : Cli_primitive.path;
|
||||
update_command : string;
|
||||
}
|
||||
|
||||
type update_status = {
|
||||
installed : bool;
|
||||
outdated : bool;
|
||||
outdated_targets : install_target Rrbvec.t;
|
||||
error : Error.t option;
|
||||
}
|
||||
|
||||
let skill_dir_name = "logseq-cli"
|
||||
let skill_file_name = "SKILL.md"
|
||||
|
||||
let skill_file_under base =
|
||||
Filename.concat
|
||||
(Filename.concat
|
||||
(Filename.concat (Filename.concat base ".agents") "skills")
|
||||
skill_dir_name)
|
||||
skill_file_name
|
||||
|
||||
external process_argv : string array = "argv" [@@mel.module "process"]
|
||||
|
||||
let executable_dir_candidates () =
|
||||
let candidates_for_script script =
|
||||
let script_dir = Filename.dirname script in
|
||||
Vec.of_array
|
||||
[|
|
||||
skill_file_under script_dir;
|
||||
skill_file_under (Filename.dirname script_dir);
|
||||
|]
|
||||
in
|
||||
if Array.length process_argv >= 2 then candidates_for_script process_argv.(1)
|
||||
else if Array.length process_argv = 1 then
|
||||
candidates_for_script process_argv.(0)
|
||||
else Vec.empty
|
||||
|
||||
let non_empty value =
|
||||
match Option.map String.trim value with
|
||||
| Some "" | None -> None
|
||||
| Some value -> Some value
|
||||
|
||||
let resolve_install_target ~global ~cwd ~home_dir =
|
||||
match (global, non_empty home_dir) with
|
||||
| true, None ->
|
||||
Error
|
||||
(Error.make Error.Skill_home_dir_unavailable
|
||||
"home directory is unavailable; cannot resolve --global install \
|
||||
target")
|
||||
| _ ->
|
||||
let base =
|
||||
if global then Option.value (non_empty home_dir) ~default:cwd else cwd
|
||||
in
|
||||
Ok
|
||||
{
|
||||
scope = Edn_util.keyword_t (if global then "global" else "local");
|
||||
path = skill_file_under base;
|
||||
update_command =
|
||||
(if global then "logseq skill install --global"
|
||||
else "logseq skill install");
|
||||
}
|
||||
|
||||
let installed_skill_targets ~cwd ~home_dir =
|
||||
let local = resolve_install_target ~global:false ~cwd ~home_dir in
|
||||
let global = resolve_install_target ~global:true ~cwd ~home_dir in
|
||||
Vec.of_array [| local; global |]
|
||||
|> Vec.filter_map (function Ok target -> Some target | Error _ -> None)
|
||||
|
||||
let read_file = Cli_unix.read_text_file
|
||||
|
||||
let rec mkdir_p path =
|
||||
if path = "" || path = Filename.dirname path || Cli_unix.file_exists path then
|
||||
()
|
||||
else (
|
||||
mkdir_p (Filename.dirname path);
|
||||
Cli_unix.mkdir path 0o755)
|
||||
|
||||
let write_file path content =
|
||||
mkdir_p (Filename.dirname path);
|
||||
Cli_unix.write_text_file path content
|
||||
|
||||
let default_source_candidates config =
|
||||
let executable = executable_dir_candidates () in
|
||||
let project =
|
||||
match config.Cli_config.project_dir with
|
||||
| Some project_dir -> Vec.singleton (skill_file_under project_dir)
|
||||
| None -> Vec.empty
|
||||
in
|
||||
let home =
|
||||
match Sys.getenv_opt "HOME" with
|
||||
| Some home -> Vec.singleton (skill_file_under home)
|
||||
| None -> Vec.empty
|
||||
in
|
||||
Vec.append executable project |> fun candidates ->
|
||||
Vec.append candidates home |> fun candidates ->
|
||||
Vec.push_back candidates (skill_file_under (Sys.getcwd ()))
|
||||
|
||||
let resolve_source_path ?source_path config =
|
||||
match non_empty source_path with
|
||||
| Some path when Cli_unix.file_exists path -> Ok path
|
||||
| Some path ->
|
||||
Error
|
||||
(Error.make
|
||||
~context:
|
||||
(Edn_util.vector_vec (Vec.of_array [| Edn_util.string path |]))
|
||||
Error.Skill_source_not_found
|
||||
("skill source file not found: " ^ path))
|
||||
| None -> (
|
||||
let candidates = default_source_candidates config in
|
||||
match Vec.find_opt Cli_unix.file_exists candidates with
|
||||
| Some path -> Ok path
|
||||
| None ->
|
||||
Error
|
||||
(Error.make
|
||||
~context:
|
||||
(Edn_util.vector_vec
|
||||
(candidates |> Vec.map (fun path -> Edn_util.string path)))
|
||||
Error.Skill_source_not_found
|
||||
("skill source file not found. Checked paths: "
|
||||
^ Vec.string_concat ", " candidates)))
|
||||
|
||||
let command_id = function
|
||||
| Parsed_show -> Command_id.Skill_show
|
||||
| Parsed_install _ -> Skill_install
|
||||
|
||||
let validate_parsed _ = Ok ()
|
||||
|
||||
let build ?registry:_ config _globals = function
|
||||
| Parsed_show -> Ok (Skill_show { source_path = None })
|
||||
| Parsed_install { global } ->
|
||||
let cwd =
|
||||
Option.value config.Cli_config.project_dir ~default:(Sys.getcwd ())
|
||||
in
|
||||
let home_dir = Sys.getenv_opt "HOME" in
|
||||
Error.bind (resolve_install_target ~global ~cwd ~home_dir) (fun target ->
|
||||
Ok
|
||||
(Skill_install
|
||||
{
|
||||
global;
|
||||
source_path = None;
|
||||
destination_dir = Some (Filename.dirname target.path);
|
||||
destination_file = Some target.path;
|
||||
}))
|
||||
|
||||
let target_from_action config global destination_dir destination_file =
|
||||
match (destination_dir, destination_file) with
|
||||
| Some dir, Some file -> Ok (dir, file)
|
||||
| _ ->
|
||||
let cwd =
|
||||
Option.value config.Cli_config.project_dir ~default:(Sys.getcwd ())
|
||||
in
|
||||
Error.bind
|
||||
(resolve_install_target ~global ~cwd ~home_dir:(Sys.getenv_opt "HOME"))
|
||||
(fun target -> Ok (Filename.dirname target.path, target.path))
|
||||
|
||||
let execute_show config mode source_path =
|
||||
let command = Command_id.Skill_show in
|
||||
match resolve_source_path ?source_path config with
|
||||
| Error err -> Cli_effect.pure (Output_mode.error ~command mode err)
|
||||
| Ok path -> (
|
||||
match read_file path with
|
||||
| content ->
|
||||
Cli_effect.pure (Cli_result.ok ~command mode (Message content))
|
||||
| exception exn ->
|
||||
Cli_effect.pure
|
||||
(Output_mode.error ~command mode
|
||||
(Error.make Error.Skill_show_failed
|
||||
("failed to read skill source: " ^ Printexc.to_string exn))))
|
||||
|
||||
let install_value ~source ~file =
|
||||
Edn_util.map_vec
|
||||
(Vec.of_array
|
||||
[|
|
||||
(Edn_util.keyword "source-path", Edn_util.string source);
|
||||
(Edn_util.keyword "installed-path", Edn_util.string file);
|
||||
( Edn_util.keyword "message",
|
||||
Edn_util.string ("Installed skill to " ^ file) );
|
||||
|])
|
||||
|
||||
let execute_install config mode ~global ~source_path ~destination_dir
|
||||
~destination_file =
|
||||
let command = Command_id.Skill_install in
|
||||
match resolve_source_path ?source_path config with
|
||||
| Error err -> Cli_effect.pure (Output_mode.error ~command mode err)
|
||||
| Ok source -> (
|
||||
match
|
||||
target_from_action config global destination_dir destination_file
|
||||
with
|
||||
| Error err -> Cli_effect.pure (Output_mode.error ~command mode err)
|
||||
| Ok (dir, file) -> (
|
||||
try
|
||||
if Cli_unix.file_exists dir && not (Cli_unix.is_directory dir) then
|
||||
raise (Invalid_argument ("destination path is a file: " ^ dir));
|
||||
let content = read_file source in
|
||||
write_file file content;
|
||||
Cli_effect.pure
|
||||
(Cli_result.ok ~command mode (Raw (install_value ~source ~file)))
|
||||
with exn ->
|
||||
Cli_effect.pure
|
||||
(Output_mode.error ~command mode
|
||||
(Error.make Error.Skill_install_failed
|
||||
("failed to install skill: " ^ Printexc.to_string exn)))))
|
||||
|
||||
let execute_with_mode action config mode =
|
||||
match action with
|
||||
| Skill_show { source_path } -> execute_show config mode source_path
|
||||
| Skill_install { global; source_path; destination_dir; destination_file } ->
|
||||
execute_install config mode ~global ~source_path ~destination_dir
|
||||
~destination_file
|
||||
|
||||
let meta id doc =
|
||||
{
|
||||
Command_registry.id;
|
||||
path = Command_id.to_path id;
|
||||
doc;
|
||||
long_doc = None;
|
||||
examples = Vec.empty;
|
||||
options = Vec.empty;
|
||||
category = Command_registry.Utilities;
|
||||
requires_graph = Command_id.requires_graph id;
|
||||
requires_auth = Command_id.requires_auth id;
|
||||
write_command = Command_id.is_write id;
|
||||
human_table_headers_order = Vec.empty;
|
||||
}
|
||||
|
||||
let metadata () =
|
||||
Vec.of_array
|
||||
[|
|
||||
meta Command_id.Skill_show "Show built-in logseq-cli skill";
|
||||
meta Skill_install "Install built-in logseq-cli skill";
|
||||
|]
|
||||
|
||||
let execute action config =
|
||||
let (Output.Mode.Packed mode) = Output_mode.for_config config in
|
||||
execute_with_mode action config mode
|
||||
Reference in New Issue
Block a user