akm-cli 0.7.5 → 0.8.0-rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/CHANGELOG.md +1 -1
- package/dist/cli/parse-args.js +43 -0
- package/dist/cli.js +853 -479
- package/dist/commands/agent-dispatch.js +102 -0
- package/dist/commands/agent-support.js +62 -0
- package/dist/commands/config-cli.js +68 -84
- package/dist/commands/consolidate.js +823 -0
- package/dist/commands/distill-promotion-policy.js +658 -0
- package/dist/commands/distill.js +244 -52
- package/dist/commands/eval-cases.js +40 -0
- package/dist/commands/events.js +2 -23
- package/dist/commands/graph.js +222 -0
- package/dist/commands/health.js +376 -0
- package/dist/commands/help/help-accept.md +9 -0
- package/dist/commands/help/help-improve.md +53 -0
- package/dist/commands/help/help-proposals.md +15 -0
- package/dist/commands/help/help-propose.md +17 -0
- package/dist/commands/help/help-reject.md +8 -0
- package/dist/commands/history.js +3 -30
- package/dist/commands/improve.js +1170 -0
- package/dist/commands/info.js +2 -2
- package/dist/commands/init.js +2 -2
- package/dist/commands/install-audit.js +5 -1
- package/dist/commands/installed-stashes.js +118 -138
- package/dist/commands/knowledge.js +133 -0
- package/dist/commands/lint/agent-linter.js +46 -0
- package/dist/commands/lint/base-linter.js +285 -0
- package/dist/commands/lint/command-linter.js +46 -0
- package/dist/commands/lint/default-linter.js +13 -0
- package/dist/commands/lint/index.js +107 -0
- package/dist/commands/lint/knowledge-linter.js +13 -0
- package/dist/commands/lint/memory-linter.js +58 -0
- package/dist/commands/lint/registry.js +33 -0
- package/dist/commands/lint/skill-linter.js +42 -0
- package/dist/commands/lint/task-linter.js +47 -0
- package/dist/commands/lint/types.js +1 -0
- package/dist/commands/lint/workflow-linter.js +53 -0
- package/dist/commands/lint.js +1 -0
- package/dist/commands/proposal.js +8 -7
- package/dist/commands/propose.js +78 -28
- package/dist/commands/reflect.js +143 -35
- package/dist/commands/registry-search.js +2 -2
- package/dist/commands/remember.js +54 -0
- package/dist/commands/schema-repair.js +130 -0
- package/dist/commands/search.js +21 -5
- package/dist/commands/show.js +121 -17
- package/dist/commands/source-add.js +10 -10
- package/dist/commands/source-manage.js +11 -19
- package/dist/commands/tasks.js +385 -0
- package/dist/commands/url-checker.js +39 -0
- package/dist/commands/vault.js +8 -26
- package/dist/core/action-contributors.js +25 -0
- package/dist/core/asset-ref.js +4 -0
- package/dist/core/asset-registry.js +4 -16
- package/dist/core/asset-spec.js +10 -0
- package/dist/core/common.js +94 -0
- package/dist/core/concurrent.js +22 -0
- package/dist/core/config.js +222 -128
- package/dist/core/events.js +73 -126
- package/dist/core/frontmatter.js +3 -1
- package/dist/core/markdown.js +17 -0
- package/dist/core/memory-improve.js +678 -0
- package/dist/core/parse.js +155 -0
- package/dist/core/paths.js +101 -3
- package/dist/core/proposal-validators.js +61 -0
- package/dist/core/proposals.js +49 -38
- package/dist/core/state-db.js +775 -0
- package/dist/core/time.js +51 -0
- package/dist/core/warn.js +59 -1
- package/dist/indexer/db-search.js +52 -238
- package/dist/indexer/db.js +378 -1
- package/dist/indexer/ensure-index.js +61 -0
- package/dist/indexer/graph-boost.js +247 -94
- package/dist/indexer/graph-db.js +201 -0
- package/dist/indexer/graph-dedup.js +99 -0
- package/dist/indexer/graph-extraction.js +409 -76
- package/dist/indexer/index-context.js +10 -0
- package/dist/indexer/indexer.js +442 -290
- package/dist/indexer/llm-cache.js +47 -0
- package/dist/indexer/match-contributors.js +141 -0
- package/dist/indexer/matchers.js +24 -190
- package/dist/indexer/memory-inference.js +63 -29
- package/dist/indexer/metadata-contributors.js +26 -0
- package/dist/indexer/metadata.js +194 -175
- package/dist/indexer/path-resolver.js +89 -0
- package/dist/indexer/ranking-contributors.js +204 -0
- package/dist/indexer/ranking.js +74 -0
- package/dist/indexer/search-hit-enrichers.js +22 -0
- package/dist/indexer/search-source.js +24 -9
- package/dist/indexer/semantic-status.js +2 -16
- package/dist/indexer/walker.js +25 -0
- package/dist/integrations/agent/config.js +175 -3
- package/dist/integrations/agent/index.js +3 -1
- package/dist/integrations/agent/pipeline.js +39 -0
- package/dist/integrations/agent/profiles.js +67 -5
- package/dist/integrations/agent/prompts.js +77 -72
- package/dist/integrations/agent/runners.js +31 -0
- package/dist/integrations/agent/sdk-runner.js +120 -0
- package/dist/integrations/agent/spawn.js +71 -16
- package/dist/integrations/lockfile.js +10 -18
- package/dist/integrations/session-logs/index.js +65 -0
- package/dist/integrations/session-logs/providers/claude-code.js +56 -0
- package/dist/integrations/session-logs/providers/opencode.js +52 -0
- package/dist/integrations/session-logs/types.js +1 -0
- package/dist/llm/call-ai.js +74 -0
- package/dist/llm/client.js +61 -122
- package/dist/llm/feature-gate.js +27 -16
- package/dist/llm/graph-extract.js +297 -62
- package/dist/llm/memory-infer.js +49 -71
- package/dist/llm/metadata-enhance.js +39 -22
- package/dist/llm/prompts/graph-extract-user-prompt.md +12 -0
- package/dist/output/cli-hints-full.md +277 -0
- package/dist/output/cli-hints-short.md +65 -0
- package/dist/output/cli-hints.js +2 -318
- package/dist/output/renderers.js +190 -123
- package/dist/output/shapes.js +33 -0
- package/dist/output/text.js +239 -2
- package/dist/registry/providers/skills-sh.js +61 -49
- package/dist/registry/providers/static-index.js +44 -48
- package/dist/setup/setup.js +510 -11
- package/dist/sources/provider-factory.js +2 -1
- package/dist/sources/providers/git.js +2 -2
- package/dist/sources/website-ingest.js +4 -0
- package/dist/tasks/backends/cron.js +200 -0
- package/dist/tasks/backends/exec-utils.js +25 -0
- package/dist/tasks/backends/index.js +32 -0
- package/dist/tasks/backends/launchd-template.xml +19 -0
- package/dist/tasks/backends/launchd.js +184 -0
- package/dist/tasks/backends/schtasks-template.xml +29 -0
- package/dist/tasks/backends/schtasks.js +212 -0
- package/dist/tasks/parser.js +198 -0
- package/dist/tasks/resolveAkmBin.js +84 -0
- package/dist/tasks/runner.js +432 -0
- package/dist/tasks/schedule.js +208 -0
- package/dist/tasks/schema.js +13 -0
- package/dist/tasks/validator.js +59 -0
- package/dist/wiki/index-template.md +12 -0
- package/dist/wiki/ingest-workflow-template.md +54 -0
- package/dist/wiki/log-template.md +8 -0
- package/dist/wiki/schema-template.md +61 -0
- package/dist/wiki/wiki-templates.js +12 -0
- package/dist/wiki/wiki.js +10 -61
- package/dist/workflows/authoring.js +5 -25
- package/dist/workflows/renderer.js +8 -3
- package/dist/workflows/runs.js +59 -91
- package/dist/workflows/validator.js +1 -1
- package/dist/workflows/workflow-template.md +24 -0
- package/docs/README.md +3 -0
- package/docs/migration/release-notes/0.7.0.md +1 -1
- package/docs/migration/release-notes/0.8.0.md +43 -0
- package/package.json +3 -2
- package/dist/templates/wiki-templates.js +0 -100
package/dist/setup/setup.js
CHANGED
|
@@ -11,8 +11,9 @@ import path from "node:path";
|
|
|
11
11
|
import * as p from "@clack/prompts";
|
|
12
12
|
import { akmInit } from "../commands/init";
|
|
13
13
|
import { isHttpUrl } from "../core/common";
|
|
14
|
-
import { DEFAULT_CONFIG,
|
|
15
|
-
import { getDefaultStashDir } from "../core/paths";
|
|
14
|
+
import { DEFAULT_CONFIG, loadUserConfig, saveConfig } from "../core/config";
|
|
15
|
+
import { getConfigPath, getDefaultStashDir } from "../core/paths";
|
|
16
|
+
import { warn } from "../core/warn";
|
|
16
17
|
import { closeDatabase, isVecAvailable, openDatabase } from "../indexer/db";
|
|
17
18
|
import { akmIndex } from "../indexer/indexer";
|
|
18
19
|
import { clearSemanticStatus, deriveSemanticProviderFingerprint, writeSemanticStatus, } from "../indexer/semantic-status";
|
|
@@ -337,8 +338,11 @@ async function prepareSemanticSearchAssets(config) {
|
|
|
337
338
|
return { ok: true };
|
|
338
339
|
}
|
|
339
340
|
// ── Steps ───────────────────────────────────────────────────────────────────
|
|
340
|
-
async function stepStashDir(current) {
|
|
341
|
-
const defaultDir = current.stashDir ?? getDefaultStashDir();
|
|
341
|
+
async function stepStashDir(current, options) {
|
|
342
|
+
const defaultDir = options?.preferredDir ?? current.stashDir ?? getDefaultStashDir();
|
|
343
|
+
if (options?.nonInteractive) {
|
|
344
|
+
return defaultDir;
|
|
345
|
+
}
|
|
342
346
|
const choice = await prompt(() => p.select({
|
|
343
347
|
message: "Where should akm store skills, commands, and other assets?",
|
|
344
348
|
options: [
|
|
@@ -635,7 +639,7 @@ export async function stepRegistries(current) {
|
|
|
635
639
|
* @internal Exported for testing only.
|
|
636
640
|
*/
|
|
637
641
|
export async function stepAddSources(current, options) {
|
|
638
|
-
const existingSources = [...(current.sources ??
|
|
642
|
+
const existingSources = [...(current.sources ?? [])];
|
|
639
643
|
const sources = [];
|
|
640
644
|
if (existingSources.length > 0) {
|
|
641
645
|
p.note(renderConfiguredSourceList(existingSources), "Configured stash sources");
|
|
@@ -705,7 +709,7 @@ async function stepAgentPlatforms(current) {
|
|
|
705
709
|
p.log.info("No agent platform configurations detected.");
|
|
706
710
|
return [];
|
|
707
711
|
}
|
|
708
|
-
const existingPaths = new Set((current.sources ??
|
|
712
|
+
const existingPaths = new Set((current.sources ?? []).map((s) => s.path));
|
|
709
713
|
// Filter out platforms already configured
|
|
710
714
|
const newPlatforms = platforms.filter((pl) => !existingPaths.has(pl.path));
|
|
711
715
|
if (newPlatforms.length === 0) {
|
|
@@ -734,6 +738,365 @@ async function stepAgentPlatforms(current) {
|
|
|
734
738
|
}
|
|
735
739
|
return entries;
|
|
736
740
|
}
|
|
741
|
+
/**
|
|
742
|
+
* Step 1/2: Configure the small model connection used for metadata and bounded LLM features.
|
|
743
|
+
*
|
|
744
|
+
* Detects Ollama automatically and pre-selects it. The user may also choose
|
|
745
|
+
* OpenAI, LM Studio, a custom endpoint, or skip the step entirely.
|
|
746
|
+
*/
|
|
747
|
+
export async function stepSmallModelConnection(current) {
|
|
748
|
+
p.log.step("Step 1/2: Configure your small model connection");
|
|
749
|
+
p.note([
|
|
750
|
+
"This connection is used for background processing:",
|
|
751
|
+
" • akm index (metadata enhancement)",
|
|
752
|
+
" • akm distill (lesson distillation)",
|
|
753
|
+
" • akm remember --enrich (memory compression)",
|
|
754
|
+
" • akm curate --rerank (search reranking)",
|
|
755
|
+
].join("\n"));
|
|
756
|
+
// Probe for Ollama in the background while showing the note.
|
|
757
|
+
const spin = p.spinner();
|
|
758
|
+
spin.start("Detecting local services...");
|
|
759
|
+
const ollama = await detectOllama();
|
|
760
|
+
spin.stop(ollama.available ? `Ollama detected at ${ollama.endpoint}` : "No local services detected");
|
|
761
|
+
const ollamaEndpoint = ollama.available ? ollama.endpoint : undefined;
|
|
762
|
+
const providerOptions = [];
|
|
763
|
+
if (ollama.available) {
|
|
764
|
+
providerOptions.push({
|
|
765
|
+
value: "ollama",
|
|
766
|
+
label: "Ollama (local)",
|
|
767
|
+
hint: `detected at ${ollama.endpoint}`,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
providerOptions.push({ value: "openai", label: "OpenAI", hint: "requires AKM_LLM_API_KEY" }, { value: "lmstudio", label: "LM Studio / local server", hint: "http://localhost:1234" }, { value: "custom", label: "Custom OpenAI-compatible endpoint" }, { value: "skip", label: "Skip — disable enrichment features" });
|
|
771
|
+
if (current.llm) {
|
|
772
|
+
providerOptions.push({
|
|
773
|
+
value: "keep",
|
|
774
|
+
label: `Keep current: ${current.llm.provider ?? current.llm.endpoint}`,
|
|
775
|
+
hint: current.llm.model,
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
const initialValue = current.llm ? "keep" : ollama.available ? "ollama" : "openai";
|
|
779
|
+
const providerChoice = await prompt(() => p.select({
|
|
780
|
+
message: "Provider:",
|
|
781
|
+
options: providerOptions,
|
|
782
|
+
initialValue,
|
|
783
|
+
}));
|
|
784
|
+
if (providerChoice === "keep") {
|
|
785
|
+
return { llm: cloneLlmConfig(current.llm), skipped: false, ollamaEndpoint };
|
|
786
|
+
}
|
|
787
|
+
if (providerChoice === "skip") {
|
|
788
|
+
p.note([
|
|
789
|
+
"Enrichment features disabled:",
|
|
790
|
+
" • akm index — metadata enhancement disabled",
|
|
791
|
+
" • akm distill — lesson generation",
|
|
792
|
+
" • akm remember --enrich",
|
|
793
|
+
" • akm curate --rerank",
|
|
794
|
+
"",
|
|
795
|
+
"You can configure this later with `akm setup`.",
|
|
796
|
+
].join("\n"), "Warning");
|
|
797
|
+
return { llm: undefined, skipped: true, ollamaEndpoint };
|
|
798
|
+
}
|
|
799
|
+
let llm;
|
|
800
|
+
if (providerChoice === "ollama") {
|
|
801
|
+
const ollamaChatModels = ollama.models.filter((m) => !m.includes("embed") && !m.includes("nomic") && !m.includes("minilm") && !m.includes("bge"));
|
|
802
|
+
let model;
|
|
803
|
+
if (ollamaChatModels.length > 0) {
|
|
804
|
+
model = await prompt(() => p.select({
|
|
805
|
+
message: "Model name:",
|
|
806
|
+
options: [
|
|
807
|
+
...ollamaChatModels.map((m) => ({ value: m, label: m })),
|
|
808
|
+
{ value: "__custom__", label: "Enter a model name manually..." },
|
|
809
|
+
],
|
|
810
|
+
initialValue: ollamaChatModels[0],
|
|
811
|
+
}));
|
|
812
|
+
if (model === "__custom__") {
|
|
813
|
+
model = await prompt(() => p.text({
|
|
814
|
+
message: "Model name:",
|
|
815
|
+
placeholder: "llama3.2",
|
|
816
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
817
|
+
}));
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
else {
|
|
821
|
+
model = await prompt(() => p.text({
|
|
822
|
+
message: "Model name (e.g. llama3.2):",
|
|
823
|
+
placeholder: "llama3.2",
|
|
824
|
+
defaultValue: "llama3.2",
|
|
825
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
826
|
+
}));
|
|
827
|
+
}
|
|
828
|
+
llm = {
|
|
829
|
+
provider: "ollama",
|
|
830
|
+
endpoint: `${ollama.endpoint}/v1/chat/completions`,
|
|
831
|
+
model: model.trim(),
|
|
832
|
+
temperature: 0.3,
|
|
833
|
+
maxTokens: 1024,
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
else if (providerChoice === "openai") {
|
|
837
|
+
const model = await prompt(() => p.text({
|
|
838
|
+
message: "Model name:",
|
|
839
|
+
placeholder: "gpt-4o-mini",
|
|
840
|
+
defaultValue: "gpt-4o-mini",
|
|
841
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
842
|
+
}));
|
|
843
|
+
if (!process.env.AKM_LLM_API_KEY) {
|
|
844
|
+
p.log.info("Set AKM_LLM_API_KEY in your shell before running `akm index`.");
|
|
845
|
+
}
|
|
846
|
+
llm = {
|
|
847
|
+
provider: "openai",
|
|
848
|
+
endpoint: "https://api.openai.com/v1/chat/completions",
|
|
849
|
+
model: model.trim() || "gpt-4o-mini",
|
|
850
|
+
temperature: 0.3,
|
|
851
|
+
maxTokens: 1024,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
else if (providerChoice === "lmstudio") {
|
|
855
|
+
const endpoint = await prompt(() => p.text({
|
|
856
|
+
message: "Endpoint URL:",
|
|
857
|
+
placeholder: "http://localhost:1234/v1/chat/completions",
|
|
858
|
+
defaultValue: "http://localhost:1234/v1/chat/completions",
|
|
859
|
+
validate: (v) => {
|
|
860
|
+
if (!v?.trim())
|
|
861
|
+
return "Endpoint cannot be empty";
|
|
862
|
+
if (!v.startsWith("http://") && !v.startsWith("https://"))
|
|
863
|
+
return "Must start with http:// or https://";
|
|
864
|
+
},
|
|
865
|
+
}));
|
|
866
|
+
const model = await prompt(() => p.text({
|
|
867
|
+
message: "Model name:",
|
|
868
|
+
placeholder: "local-model",
|
|
869
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
870
|
+
}));
|
|
871
|
+
llm = {
|
|
872
|
+
provider: "lmstudio",
|
|
873
|
+
endpoint: endpoint.trim(),
|
|
874
|
+
model: model.trim(),
|
|
875
|
+
temperature: 0.3,
|
|
876
|
+
maxTokens: 1024,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
// custom
|
|
881
|
+
const endpoint = await prompt(() => p.text({
|
|
882
|
+
message: "OpenAI-compatible chat completions endpoint:",
|
|
883
|
+
placeholder: "https://your-host/v1/chat/completions",
|
|
884
|
+
validate: (v) => {
|
|
885
|
+
if (!v?.trim())
|
|
886
|
+
return "Endpoint cannot be empty";
|
|
887
|
+
if (!v.startsWith("http://") && !v.startsWith("https://"))
|
|
888
|
+
return "Must start with http:// or https://";
|
|
889
|
+
},
|
|
890
|
+
}));
|
|
891
|
+
const model = await prompt(() => p.text({
|
|
892
|
+
message: "Model name:",
|
|
893
|
+
placeholder: "gpt-4o-mini",
|
|
894
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
895
|
+
}));
|
|
896
|
+
const apiKeyInput = await promptOrBack(() => p.text({
|
|
897
|
+
message: "API key (optional — press Enter to skip):",
|
|
898
|
+
placeholder: "",
|
|
899
|
+
}));
|
|
900
|
+
llm = {
|
|
901
|
+
provider: "custom",
|
|
902
|
+
endpoint: endpoint.trim(),
|
|
903
|
+
model: model.trim(),
|
|
904
|
+
temperature: 0.3,
|
|
905
|
+
maxTokens: 1024,
|
|
906
|
+
...(apiKeyInput?.trim() ? { apiKey: apiKeyInput.trim() } : {}),
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
// Best-effort probe — never blocks setup.
|
|
910
|
+
const probeSpin = p.spinner();
|
|
911
|
+
probeSpin.start("Probing LLM (structured-output round-trip)...");
|
|
912
|
+
const probe = await probeLlmCapabilities(llm);
|
|
913
|
+
if (probe.reachable && probe.structuredOutput) {
|
|
914
|
+
probeSpin.stop("LLM reachable; structured output verified.");
|
|
915
|
+
llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: true };
|
|
916
|
+
}
|
|
917
|
+
else if (probe.reachable) {
|
|
918
|
+
probeSpin.stop("LLM reachable but structured-output probe failed.");
|
|
919
|
+
llm.capabilities = { ...(llm.capabilities ?? {}), structuredOutput: false };
|
|
920
|
+
}
|
|
921
|
+
else {
|
|
922
|
+
probeSpin.stop("LLM not reachable.");
|
|
923
|
+
p.log.warn(`Could not reach the LLM endpoint${probe.error ? ` (${probe.error})` : ""}. Configuration was saved; verify your endpoint and API key, then retry.`);
|
|
924
|
+
}
|
|
925
|
+
return { llm, skipped: false, ollamaEndpoint };
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Step 2/2: Configure the agent connection used for agentic features.
|
|
929
|
+
*
|
|
930
|
+
* Options depend on whether Step 1 was completed or skipped.
|
|
931
|
+
*/
|
|
932
|
+
export async function stepAgentConnection(current, smallModel) {
|
|
933
|
+
p.log.step("Step 2/2: Configure your agent connection");
|
|
934
|
+
p.note([
|
|
935
|
+
"This connection is used for agentic commands:",
|
|
936
|
+
" • akm propose (generate improvement proposals)",
|
|
937
|
+
" • akm reflect (reflect on assets and generate proposals)",
|
|
938
|
+
" • akm tasks run (run automated task prompts)",
|
|
939
|
+
].join("\n"));
|
|
940
|
+
// Detect available CLI agents.
|
|
941
|
+
const detections = detectAgentCliProfiles(current.agent);
|
|
942
|
+
const availableClis = detections.filter((d) => d.available);
|
|
943
|
+
const agentOptions = [];
|
|
944
|
+
if (!smallModel.skipped && smallModel.llm) {
|
|
945
|
+
agentOptions.push({
|
|
946
|
+
value: "same-connection",
|
|
947
|
+
label: "Same connection, select model",
|
|
948
|
+
hint: `uses ${smallModel.llm.endpoint.replace("/v1/chat/completions", "")}`,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
agentOptions.push({ value: "new-connection", label: "New connection (different endpoint)" });
|
|
952
|
+
if (availableClis.length > 0) {
|
|
953
|
+
agentOptions.push({
|
|
954
|
+
value: "cli-agent",
|
|
955
|
+
label: "Installed CLI agent",
|
|
956
|
+
hint: `${availableClis.map((d) => d.name).join(", ")} detected`,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
agentOptions.push({ value: "none", label: "None — disable agentic features" });
|
|
960
|
+
if (current.agent) {
|
|
961
|
+
const currentDesc = current.agent.default
|
|
962
|
+
? `CLI: ${current.agent.default}`
|
|
963
|
+
: current.agent.profiles?.default?.model
|
|
964
|
+
? `SDK: ${current.agent.profiles.default.model}`
|
|
965
|
+
: "configured";
|
|
966
|
+
agentOptions.push({ value: "keep", label: `Keep current: ${currentDesc}` });
|
|
967
|
+
}
|
|
968
|
+
const initialAgentValue = current.agent
|
|
969
|
+
? "keep"
|
|
970
|
+
: availableClis.length > 0 && smallModel.skipped
|
|
971
|
+
? "cli-agent"
|
|
972
|
+
: !smallModel.skipped && smallModel.llm
|
|
973
|
+
? "same-connection"
|
|
974
|
+
: availableClis.length > 0
|
|
975
|
+
? "cli-agent"
|
|
976
|
+
: "none";
|
|
977
|
+
const agentChoice = await prompt(() => p.select({
|
|
978
|
+
message: "How do you want to run agent commands?",
|
|
979
|
+
options: agentOptions,
|
|
980
|
+
initialValue: initialAgentValue,
|
|
981
|
+
}));
|
|
982
|
+
if (agentChoice === "keep") {
|
|
983
|
+
return current.agent;
|
|
984
|
+
}
|
|
985
|
+
if (agentChoice === "none") {
|
|
986
|
+
p.note([
|
|
987
|
+
"Agentic features disabled:",
|
|
988
|
+
' • akm propose — will show "no agent configured" error',
|
|
989
|
+
' • akm reflect — will show "no agent configured" error',
|
|
990
|
+
' • akm tasks run — will show "no agent configured" error',
|
|
991
|
+
"",
|
|
992
|
+
"You can configure this later with `akm setup`.",
|
|
993
|
+
].join("\n"), "Warning");
|
|
994
|
+
return undefined;
|
|
995
|
+
}
|
|
996
|
+
if (agentChoice === "same-connection") {
|
|
997
|
+
if (smallModel.skipped || !smallModel.llm) {
|
|
998
|
+
p.log.warn("You skipped the small model connection. Configure one to use the same connection. Falling back to 'new connection'.");
|
|
999
|
+
// Fall through to new-connection flow
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
const baseEndpoint = smallModel.llm.endpoint.replace("/v1/chat/completions", "");
|
|
1003
|
+
p.log.info(`Endpoint: ${baseEndpoint} (from Step 1)`);
|
|
1004
|
+
const agentModel = await prompt(() => p.text({
|
|
1005
|
+
message: "Model to use for agent tasks (same model is fine, larger models work better):",
|
|
1006
|
+
placeholder: "qwen2.5-coder:32b",
|
|
1007
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
1008
|
+
}));
|
|
1009
|
+
const profileName = smallModel.llm.provider ?? "default";
|
|
1010
|
+
return {
|
|
1011
|
+
...(current.agent ?? {}),
|
|
1012
|
+
profiles: {
|
|
1013
|
+
...(current.agent?.profiles ?? {}),
|
|
1014
|
+
[profileName]: {
|
|
1015
|
+
...(current.agent?.profiles?.[profileName] ?? {}),
|
|
1016
|
+
sdkMode: true,
|
|
1017
|
+
model: agentModel.trim(),
|
|
1018
|
+
endpoint: smallModel.llm.endpoint,
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
default: profileName,
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (agentChoice === "cli-agent") {
|
|
1026
|
+
if (availableClis.length === 0) {
|
|
1027
|
+
p.log.warn("No agent CLIs detected on PATH.");
|
|
1028
|
+
return current.agent;
|
|
1029
|
+
}
|
|
1030
|
+
const initialCli = pickDefaultAgentProfile(detections, current.agent?.default) ?? availableClis[0]?.name;
|
|
1031
|
+
const selectedCli = await prompt(() => p.select({
|
|
1032
|
+
message: "Which CLI agent?",
|
|
1033
|
+
options: availableClis.map((d) => ({
|
|
1034
|
+
value: d.name,
|
|
1035
|
+
label: d.name,
|
|
1036
|
+
hint: d.resolvedPath ?? d.bin,
|
|
1037
|
+
})),
|
|
1038
|
+
initialValue: initialCli,
|
|
1039
|
+
}));
|
|
1040
|
+
return {
|
|
1041
|
+
...(current.agent ?? {}),
|
|
1042
|
+
default: selectedCli,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
// "new-connection" (also fall-through from "same-provider" when Step 1 was skipped)
|
|
1046
|
+
const newEndpoint = await prompt(() => p.text({
|
|
1047
|
+
message: "OpenAI-compatible chat completions endpoint:",
|
|
1048
|
+
placeholder: "https://your-host/v1/chat/completions",
|
|
1049
|
+
validate: (v) => {
|
|
1050
|
+
if (!v?.trim())
|
|
1051
|
+
return "Endpoint cannot be empty";
|
|
1052
|
+
if (!v.startsWith("http://") && !v.startsWith("https://"))
|
|
1053
|
+
return "Must start with http:// or https://";
|
|
1054
|
+
},
|
|
1055
|
+
}));
|
|
1056
|
+
const newApiKeyInput = await promptOrBack(() => p.text({
|
|
1057
|
+
message: "API key (optional — press Enter to skip):",
|
|
1058
|
+
placeholder: "",
|
|
1059
|
+
}));
|
|
1060
|
+
const newModel = await prompt(() => p.text({
|
|
1061
|
+
message: "Model name (larger is better, e.g. gpt-4o):",
|
|
1062
|
+
placeholder: "gpt-4o",
|
|
1063
|
+
validate: (v) => (!v?.trim() ? "Model name cannot be empty" : undefined),
|
|
1064
|
+
}));
|
|
1065
|
+
const customProfile = {
|
|
1066
|
+
sdkMode: true,
|
|
1067
|
+
endpoint: newEndpoint.trim(),
|
|
1068
|
+
model: newModel.trim(),
|
|
1069
|
+
...(newApiKeyInput?.trim() ? { apiKey: newApiKeyInput.trim() } : {}),
|
|
1070
|
+
};
|
|
1071
|
+
return {
|
|
1072
|
+
...(current.agent ?? {}),
|
|
1073
|
+
profiles: {
|
|
1074
|
+
...(current.agent?.profiles ?? {}),
|
|
1075
|
+
custom: customProfile,
|
|
1076
|
+
},
|
|
1077
|
+
default: "custom",
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Print a feature capability summary after both connection steps are complete.
|
|
1082
|
+
*/
|
|
1083
|
+
function printCapabilitySummary(smallModelSkipped, agentConfigured) {
|
|
1084
|
+
const lines = ["Setup complete. Here's what's enabled:", ""];
|
|
1085
|
+
lines.push(" ✓ akm search, akm curate, akm show — always available");
|
|
1086
|
+
if (!smallModelSkipped) {
|
|
1087
|
+
lines.push(" ✓ akm index, akm distill, akm remember — small model configured");
|
|
1088
|
+
}
|
|
1089
|
+
else {
|
|
1090
|
+
lines.push(" ✗ akm index, akm distill, akm remember — run `akm setup` to enable");
|
|
1091
|
+
}
|
|
1092
|
+
if (agentConfigured) {
|
|
1093
|
+
lines.push(" ✓ akm propose, akm reflect, akm tasks — agent configured");
|
|
1094
|
+
}
|
|
1095
|
+
else {
|
|
1096
|
+
lines.push(" ✗ akm propose, akm reflect, akm tasks — run `akm setup` to enable");
|
|
1097
|
+
}
|
|
1098
|
+
p.note(lines.join("\n"), "Feature Summary");
|
|
1099
|
+
}
|
|
737
1100
|
export async function stepAgentSelection(current, detections) {
|
|
738
1101
|
const available = detections.filter((d) => d.available);
|
|
739
1102
|
if (available.length === 0) {
|
|
@@ -832,7 +1195,10 @@ export function buildSetupSteps(options) {
|
|
|
832
1195
|
label: "Stash Directory",
|
|
833
1196
|
nonInteractive: true,
|
|
834
1197
|
async run(ctx) {
|
|
835
|
-
const stashDir = await stepStashDir(ctx.config
|
|
1198
|
+
const stashDir = await stepStashDir(ctx.config, {
|
|
1199
|
+
nonInteractive: ctx.nonInteractive,
|
|
1200
|
+
preferredDir: options.preferredStashDir,
|
|
1201
|
+
});
|
|
836
1202
|
ctx.apply({ stashDir });
|
|
837
1203
|
},
|
|
838
1204
|
},
|
|
@@ -922,10 +1288,17 @@ export function buildSetupSteps(options) {
|
|
|
922
1288
|
];
|
|
923
1289
|
return { steps, outcome };
|
|
924
1290
|
}
|
|
925
|
-
export async function runSetupWizard() {
|
|
1291
|
+
export async function runSetupWizard(opts) {
|
|
926
1292
|
p.intro("akm setup");
|
|
927
1293
|
const current = loadUserConfig();
|
|
928
1294
|
const configPath = getConfigPath();
|
|
1295
|
+
// Resolve stash directory early so akmInit can run before any prompts
|
|
1296
|
+
const resolvedStashDir = opts?.dir ? path.resolve(opts.dir) : (current.stashDir ?? getDefaultStashDir());
|
|
1297
|
+
// Bootstrap directory structure before any prompts so the stash exists
|
|
1298
|
+
// even if the wizard is interrupted after this point.
|
|
1299
|
+
if (!opts?.noInit) {
|
|
1300
|
+
await akmInit({ dir: resolvedStashDir });
|
|
1301
|
+
}
|
|
929
1302
|
// Quick connectivity check — skip network-dependent steps when offline
|
|
930
1303
|
const online = await isOnline();
|
|
931
1304
|
if (!online) {
|
|
@@ -936,6 +1309,7 @@ export async function runSetupWizard() {
|
|
|
936
1309
|
const { steps, outcome } = buildSetupSteps({
|
|
937
1310
|
online,
|
|
938
1311
|
semanticSearchOutcome: { mode: current.semanticSearchMode, prepareAssets: false },
|
|
1312
|
+
preferredStashDir: resolvedStashDir,
|
|
939
1313
|
});
|
|
940
1314
|
// Wrap each step with a `p.log.step()` header so the wizard UI is
|
|
941
1315
|
// unchanged. The canonical `runSetupSteps()` runner is used directly by
|
|
@@ -948,6 +1322,15 @@ export async function runSetupWizard() {
|
|
|
948
1322
|
},
|
|
949
1323
|
}));
|
|
950
1324
|
await runSetupSteps(labeledSteps, ctx);
|
|
1325
|
+
// ── Two-step connection configuration ──────────────────────────────────────
|
|
1326
|
+
// Step 1/2: Small model connection (for enrichment features)
|
|
1327
|
+
const smallModelResult = await stepSmallModelConnection(ctx.config);
|
|
1328
|
+
if (!smallModelResult.skipped) {
|
|
1329
|
+
ctx.apply({ llm: smallModelResult.llm });
|
|
1330
|
+
}
|
|
1331
|
+
// Step 2/2: Agent connection (for agentic features)
|
|
1332
|
+
const agentConfig = await stepAgentConnection(ctx.config, smallModelResult);
|
|
1333
|
+
ctx.apply({ agent: agentConfig });
|
|
951
1334
|
const newConfig = {
|
|
952
1335
|
...ctx.config,
|
|
953
1336
|
// Preserve fields the steps don't manage explicitly.
|
|
@@ -958,7 +1341,10 @@ export async function runSetupWizard() {
|
|
|
958
1341
|
const embedding = newConfig.embedding;
|
|
959
1342
|
const llm = newConfig.llm;
|
|
960
1343
|
const registries = newConfig.registries;
|
|
961
|
-
const allStashes = newConfig.sources ??
|
|
1344
|
+
const allStashes = newConfig.sources ?? [];
|
|
1345
|
+
// Feature capability summary
|
|
1346
|
+
const agentConfigured = Boolean(agentConfig);
|
|
1347
|
+
printCapabilitySummary(smallModelResult.skipped, agentConfigured);
|
|
962
1348
|
// Confirm before saving
|
|
963
1349
|
const effectiveRegistries = registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
964
1350
|
p.note([
|
|
@@ -979,8 +1365,6 @@ export async function runSetupWizard() {
|
|
|
979
1365
|
bail();
|
|
980
1366
|
// Save config
|
|
981
1367
|
saveConfig(newConfig);
|
|
982
|
-
// Initialize stash directory
|
|
983
|
-
await akmInit({ dir: stashDir });
|
|
984
1368
|
if (semanticSearchMode.mode === "off") {
|
|
985
1369
|
clearSemanticStatus();
|
|
986
1370
|
}
|
|
@@ -1062,3 +1446,118 @@ export async function runSetupWizard() {
|
|
|
1062
1446
|
}
|
|
1063
1447
|
p.outro(`Configuration saved to ${configPath}`);
|
|
1064
1448
|
}
|
|
1449
|
+
// ── Non-interactive / scripting entry points ─────────────────────────────────
|
|
1450
|
+
/**
|
|
1451
|
+
* Run setup in non-interactive mode, applying all defaults.
|
|
1452
|
+
* Safe to call from CI or scripts. Idempotent — re-running produces the same result.
|
|
1453
|
+
*/
|
|
1454
|
+
export async function runSetupWithDefaults(opts) {
|
|
1455
|
+
const current = loadUserConfig();
|
|
1456
|
+
const stashDir = opts.dir ? path.resolve(opts.dir) : (current.stashDir ?? getDefaultStashDir());
|
|
1457
|
+
// Bootstrap directory structure first
|
|
1458
|
+
let initResult;
|
|
1459
|
+
if (!opts.noInit) {
|
|
1460
|
+
initResult = await akmInit({ dir: stashDir });
|
|
1461
|
+
}
|
|
1462
|
+
// Run steps in non-interactive mode (applies defaults, skips prompts)
|
|
1463
|
+
const ctx = createSetupContext(current, { nonInteractive: true });
|
|
1464
|
+
const { steps } = buildSetupSteps({
|
|
1465
|
+
online: false,
|
|
1466
|
+
semanticSearchOutcome: { mode: current.semanticSearchMode, prepareAssets: false },
|
|
1467
|
+
preferredStashDir: stashDir,
|
|
1468
|
+
});
|
|
1469
|
+
await runSetupSteps(steps, ctx);
|
|
1470
|
+
// Ensure stashDir is set
|
|
1471
|
+
if (!ctx.config.stashDir)
|
|
1472
|
+
ctx.apply({ stashDir });
|
|
1473
|
+
// Auto-detect agent CLI if not already configured
|
|
1474
|
+
if (!ctx.config.agent) {
|
|
1475
|
+
const detected = detectAgentCliProfiles(undefined);
|
|
1476
|
+
const defaultProfile = pickDefaultAgentProfile(detected, undefined);
|
|
1477
|
+
if (defaultProfile) {
|
|
1478
|
+
ctx.apply({ agent: { default: defaultProfile } });
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
saveConfig(ctx.config);
|
|
1482
|
+
return {
|
|
1483
|
+
configPath: getConfigPath(),
|
|
1484
|
+
stashDir,
|
|
1485
|
+
stashCreated: initResult?.created ?? false,
|
|
1486
|
+
written: true,
|
|
1487
|
+
fields: Object.keys(ctx.config).filter((k) => ctx.config[k] !== undefined),
|
|
1488
|
+
ripgrep: initResult?.ripgrep,
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Apply a JSON config blob non-interactively, merging it with the current config.
|
|
1493
|
+
* Validates required sub-fields and strips unknown/restricted keys.
|
|
1494
|
+
*/
|
|
1495
|
+
export async function runSetupFromConfig(opts) {
|
|
1496
|
+
// Phase 1: Parse JSON
|
|
1497
|
+
let incoming;
|
|
1498
|
+
try {
|
|
1499
|
+
incoming = JSON.parse(opts.configJson);
|
|
1500
|
+
}
|
|
1501
|
+
catch (e) {
|
|
1502
|
+
throw new Error(`Invalid JSON in --config: ${e.message}`);
|
|
1503
|
+
}
|
|
1504
|
+
// Phase 2: Validate — only allow safe top-level keys
|
|
1505
|
+
const ALLOWED_KEYS = new Set(["stashDir", "llm", "embedding", "agent", "semanticSearchMode", "output"]);
|
|
1506
|
+
for (const key of Object.keys(incoming)) {
|
|
1507
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
1508
|
+
warn(`[akm setup] Ignoring unknown or restricted config key: "${key}"`);
|
|
1509
|
+
delete incoming[key];
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
// Validate required sub-fields
|
|
1513
|
+
if (incoming.llm) {
|
|
1514
|
+
if (!incoming.llm.endpoint?.trim())
|
|
1515
|
+
throw new Error("llm.endpoint is required when llm is provided");
|
|
1516
|
+
if (!incoming.llm.model?.trim())
|
|
1517
|
+
throw new Error("llm.model is required when llm is provided");
|
|
1518
|
+
}
|
|
1519
|
+
if (incoming.embedding) {
|
|
1520
|
+
if (!incoming.embedding.endpoint?.trim())
|
|
1521
|
+
throw new Error("embedding.endpoint is required when embedding is provided");
|
|
1522
|
+
if (!incoming.embedding.model?.trim())
|
|
1523
|
+
throw new Error("embedding.model is required when embedding is provided");
|
|
1524
|
+
}
|
|
1525
|
+
// Phase 3: Merge with existing config
|
|
1526
|
+
const current = loadUserConfig();
|
|
1527
|
+
const stashDir = opts.dir
|
|
1528
|
+
? path.resolve(opts.dir)
|
|
1529
|
+
: incoming.stashDir
|
|
1530
|
+
? path.resolve(incoming.stashDir)
|
|
1531
|
+
: (current.stashDir ?? getDefaultStashDir());
|
|
1532
|
+
const merged = {
|
|
1533
|
+
...current,
|
|
1534
|
+
...incoming,
|
|
1535
|
+
stashDir,
|
|
1536
|
+
};
|
|
1537
|
+
// Bootstrap directory structure
|
|
1538
|
+
let initResult;
|
|
1539
|
+
if (!opts.noInit) {
|
|
1540
|
+
initResult = await akmInit({ dir: stashDir });
|
|
1541
|
+
}
|
|
1542
|
+
// Optional probe
|
|
1543
|
+
if (opts.probe && merged.llm) {
|
|
1544
|
+
try {
|
|
1545
|
+
const caps = await probeLlmCapabilities(merged.llm);
|
|
1546
|
+
if (caps.reachable) {
|
|
1547
|
+
merged.llm = { ...merged.llm, capabilities: { structuredOutput: caps.structuredOutput ?? false } };
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
catch {
|
|
1551
|
+
// Non-fatal: probe failure is informational only
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
saveConfig(merged);
|
|
1555
|
+
return {
|
|
1556
|
+
configPath: getConfigPath(),
|
|
1557
|
+
stashDir,
|
|
1558
|
+
stashCreated: initResult?.created ?? false,
|
|
1559
|
+
written: true,
|
|
1560
|
+
fields: Object.keys(incoming).filter((k) => incoming[k] !== undefined),
|
|
1561
|
+
ripgrep: initResult?.ripgrep,
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Both share `create-provider-registry.ts` for the underlying string→factory
|
|
10
10
|
* map.
|
|
11
11
|
*/
|
|
12
|
+
import { getSources } from "../core/config";
|
|
12
13
|
import { createProviderRegistry } from "../registry/create-provider-registry";
|
|
13
14
|
// ── Factory map ─────────────────────────────────────────────────────────────
|
|
14
15
|
const registry = createProviderRegistry();
|
|
@@ -24,7 +25,7 @@ export function resolveSourceProviderFactory(type) {
|
|
|
24
25
|
*/
|
|
25
26
|
export function resolveSourceProviders(config) {
|
|
26
27
|
const providers = [];
|
|
27
|
-
for (const entry of config
|
|
28
|
+
for (const entry of getSources(config)) {
|
|
28
29
|
if (entry.enabled === false)
|
|
29
30
|
continue;
|
|
30
31
|
const factory = registry.resolve(entry.type);
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { TYPE_DIRS } from "../../core/asset-spec";
|
|
6
6
|
import { resolveStashDir } from "../../core/common";
|
|
7
|
-
import { loadConfig } from "../../core/config";
|
|
7
|
+
import { getSources, loadConfig } from "../../core/config";
|
|
8
8
|
import { ConfigError, UsageError } from "../../core/errors";
|
|
9
9
|
import { getRegistryCacheDir, getRegistryIndexCacheDir } from "../../core/paths";
|
|
10
10
|
import { sanitizeCommitMessage } from "../../core/write-source";
|
|
@@ -407,7 +407,7 @@ export function saveGitStash(name, message, writableOverride) {
|
|
|
407
407
|
let writable = false;
|
|
408
408
|
if (name) {
|
|
409
409
|
const config = loadConfig();
|
|
410
|
-
const stash = findGitStashByTarget(config
|
|
410
|
+
const stash = findGitStashByTarget(getSources(config), name);
|
|
411
411
|
if (!stash)
|
|
412
412
|
throw new UsageError(`No git stash found with name "${name}"`);
|
|
413
413
|
if (!GIT_STASH_TYPES.has(stash.type)) {
|
|
@@ -167,6 +167,10 @@ async function crawlWebsite(startUrl, options) {
|
|
|
167
167
|
return pages;
|
|
168
168
|
}
|
|
169
169
|
async function fetchWebsitePage(pageUrl) {
|
|
170
|
+
const parsedUrl = new URL(pageUrl);
|
|
171
|
+
if (parsedUrl.hostname.endsWith(".invalid")) {
|
|
172
|
+
throw new Error(`Refusing to fetch reserved invalid hostname: ${parsedUrl.hostname}`);
|
|
173
|
+
}
|
|
170
174
|
const response = await fetchWithRetry(pageUrl, {
|
|
171
175
|
headers: {
|
|
172
176
|
Accept: "text/html, text/markdown, text/plain;q=0.9, application/xhtml+xml;q=0.8",
|