akm-cli 0.1.2 → 0.1.3
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/README.md +4 -1
- package/dist/cli.js +13 -0
- package/dist/detect.js +120 -0
- package/dist/setup.js +506 -0
- package/dist/stash-providers/context-hub.js +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -25,12 +25,15 @@ Upgrade in place with `akm upgrade`.
|
|
|
25
25
|
## Quick Start
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
akm
|
|
28
|
+
akm setup # Guided setup: configure, initialize, and index
|
|
29
29
|
akm add github:owner/repo # Add a kit from GitHub
|
|
30
30
|
akm search "deploy" # Find assets
|
|
31
31
|
akm show script:deploy.sh # View details and run command
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
If you want to skip the wizard, `akm init --dir ~/custom-stash` initializes the
|
|
35
|
+
working stash at a custom path.
|
|
36
|
+
|
|
34
37
|
## Features
|
|
35
38
|
|
|
36
39
|
### Works with Any AI Agent
|
package/dist/cli.js
CHANGED
|
@@ -405,6 +405,18 @@ function formatSearchPlain(r, detail) {
|
|
|
405
405
|
* - registry-* : Kit discovery from remote registries (npm, GitHub)
|
|
406
406
|
* - installed-kits : Management of kits already installed locally
|
|
407
407
|
*/
|
|
408
|
+
const setupCommand = defineCommand({
|
|
409
|
+
meta: {
|
|
410
|
+
name: "setup",
|
|
411
|
+
description: "Interactive configuration wizard for embeddings, LLM, registries, and stash sources",
|
|
412
|
+
},
|
|
413
|
+
async run() {
|
|
414
|
+
await runWithJsonErrors(async () => {
|
|
415
|
+
const { runSetupWizard } = await import("./setup");
|
|
416
|
+
await runSetupWizard();
|
|
417
|
+
});
|
|
418
|
+
},
|
|
419
|
+
});
|
|
408
420
|
const initCommand = defineCommand({
|
|
409
421
|
meta: {
|
|
410
422
|
name: "init",
|
|
@@ -977,6 +989,7 @@ const main = defineCommand({
|
|
|
977
989
|
quiet: { type: "boolean", alias: "q", description: "Suppress stderr warnings", default: false },
|
|
978
990
|
},
|
|
979
991
|
subCommands: {
|
|
992
|
+
setup: setupCommand,
|
|
980
993
|
init: initCommand,
|
|
981
994
|
index: indexCommand,
|
|
982
995
|
add: addCommand,
|
package/dist/detect.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service detection utilities for the setup wizard.
|
|
3
|
+
*
|
|
4
|
+
* Pure detection functions with no user interaction — each returns
|
|
5
|
+
* a result object describing what was found.
|
|
6
|
+
*/
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
// ── Ollama Detection ────────────────────────────────────────────────────────
|
|
10
|
+
const OLLAMA_BASE = "http://localhost:11434";
|
|
11
|
+
/**
|
|
12
|
+
* Detect if Ollama is running and list available models.
|
|
13
|
+
*
|
|
14
|
+
* Tries the HTTP API first (`/api/tags`), then falls back to `ollama list`
|
|
15
|
+
* via subprocess. Returns available models sorted alphabetically.
|
|
16
|
+
*/
|
|
17
|
+
export async function detectOllama() {
|
|
18
|
+
const result = { available: false, models: [], endpoint: OLLAMA_BASE };
|
|
19
|
+
// Try HTTP API first
|
|
20
|
+
try {
|
|
21
|
+
const response = await fetch(`${OLLAMA_BASE}/api/tags`, {
|
|
22
|
+
signal: AbortSignal.timeout(3000),
|
|
23
|
+
});
|
|
24
|
+
if (response.ok) {
|
|
25
|
+
const data = (await response.json());
|
|
26
|
+
if (Array.isArray(data.models)) {
|
|
27
|
+
result.models = data.models
|
|
28
|
+
.map((m) => (typeof m.name === "string" ? m.name.replace(/:latest$/, "") : ""))
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.sort();
|
|
31
|
+
result.available = true;
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// HTTP failed — try CLI fallback
|
|
38
|
+
}
|
|
39
|
+
// CLI fallback
|
|
40
|
+
try {
|
|
41
|
+
const proc = Bun.spawn(["ollama", "list"], {
|
|
42
|
+
stdout: "pipe",
|
|
43
|
+
stderr: "pipe",
|
|
44
|
+
});
|
|
45
|
+
const text = await new Response(proc.stdout).text();
|
|
46
|
+
const exitCode = await proc.exited;
|
|
47
|
+
if (exitCode === 0 && text.trim()) {
|
|
48
|
+
const lines = text.trim().split("\n").slice(1); // skip header
|
|
49
|
+
result.models = lines
|
|
50
|
+
.map((line) => {
|
|
51
|
+
const name = line.split(/\s+/)[0]?.replace(/:latest$/, "");
|
|
52
|
+
return name || "";
|
|
53
|
+
})
|
|
54
|
+
.filter(Boolean)
|
|
55
|
+
.sort();
|
|
56
|
+
result.available = true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Ollama not installed or not in PATH
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
// ── Agent Platform Detection ────────────────────────────────────────────────
|
|
65
|
+
const AGENT_PLATFORMS = [
|
|
66
|
+
{ name: "Claude Code", relPath: ".claude" },
|
|
67
|
+
{ name: "OpenCode", relPath: ".config/opencode" },
|
|
68
|
+
{ name: "Continue", relPath: ".continue" },
|
|
69
|
+
{ name: "Codeium / Windsurf", relPath: ".codeium" },
|
|
70
|
+
{ name: "Cursor", relPath: ".cursor" },
|
|
71
|
+
{ name: "Codex CLI", relPath: ".codex" },
|
|
72
|
+
];
|
|
73
|
+
/**
|
|
74
|
+
* Scan the user's home directory for known agent platform config directories.
|
|
75
|
+
* Supports both HOME (Unix) and USERPROFILE (Windows).
|
|
76
|
+
*/
|
|
77
|
+
export function detectAgentPlatforms() {
|
|
78
|
+
const home = process.env.HOME?.trim() || process.env.USERPROFILE?.trim();
|
|
79
|
+
if (!home)
|
|
80
|
+
return [];
|
|
81
|
+
return AGENT_PLATFORMS.filter((p) => {
|
|
82
|
+
const fullPath = path.join(home, p.relPath);
|
|
83
|
+
try {
|
|
84
|
+
return fs.statSync(fullPath).isDirectory();
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}).map((p) => ({
|
|
90
|
+
name: p.name,
|
|
91
|
+
path: path.join(home, p.relPath),
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
// ── OpenViking Detection ────────────────────────────────────────────────────
|
|
95
|
+
/**
|
|
96
|
+
* Check if an OpenViking server is reachable at the given URL.
|
|
97
|
+
* Uses the lightweight /api/v1/fs/stat endpoint (GET) rather than
|
|
98
|
+
* the search endpoint which requires a running search index.
|
|
99
|
+
*/
|
|
100
|
+
export async function detectOpenViking(url) {
|
|
101
|
+
const normalized = url.replace(/\/+$/, "");
|
|
102
|
+
try {
|
|
103
|
+
// Any HTTP response (even non-2xx) from the API endpoint means the server is reachable.
|
|
104
|
+
// Only network errors / timeouts indicate the server is truly unavailable.
|
|
105
|
+
await fetch(`${normalized}/api/v1/fs/stat?uri=${encodeURIComponent("viking://")}`, {
|
|
106
|
+
signal: AbortSignal.timeout(5000),
|
|
107
|
+
});
|
|
108
|
+
return { available: true, url: normalized };
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// stat endpoint unreachable — try root URL as fallback
|
|
112
|
+
try {
|
|
113
|
+
await fetch(normalized, { signal: AbortSignal.timeout(5000) });
|
|
114
|
+
return { available: true, url: normalized };
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return { available: false, url: normalized };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive configuration wizard for akm.
|
|
3
|
+
*
|
|
4
|
+
* Walks users through service detection, embedding/LLM setup,
|
|
5
|
+
* registry selection, stash sources, and agent platform discovery.
|
|
6
|
+
* Collects all choices and writes config once at the end.
|
|
7
|
+
*/
|
|
8
|
+
import * as p from "@clack/prompts";
|
|
9
|
+
import { DEFAULT_CONFIG, getConfigPath, loadConfig, saveConfig } from "./config";
|
|
10
|
+
import { detectAgentPlatforms, detectOllama, detectOpenViking } from "./detect";
|
|
11
|
+
import { akmIndex } from "./indexer";
|
|
12
|
+
import { akmInit } from "./init";
|
|
13
|
+
import { getDefaultStashDir } from "./paths";
|
|
14
|
+
// ── Constants ───────────────────────────────────────────────────────────────
|
|
15
|
+
/** Recommended GitHub repositories shown during setup. */
|
|
16
|
+
const RECOMMENDED_GITHUB_REPOS = [
|
|
17
|
+
{
|
|
18
|
+
url: "https://github.com/andrewyng/context-hub",
|
|
19
|
+
name: "context-hub",
|
|
20
|
+
hint: "community knowledge",
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
24
|
+
function bail() {
|
|
25
|
+
p.cancel("Setup cancelled. No changes were saved.");
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if a prompt result was cancelled (Escape). If so, ask the user
|
|
30
|
+
* whether they really want to quit. Returns true if the user chose to
|
|
31
|
+
* stay (i.e. the caller should re-prompt), or calls bail() to exit.
|
|
32
|
+
*
|
|
33
|
+
* @internal Exported for testing only.
|
|
34
|
+
*/
|
|
35
|
+
export async function onCancel(value) {
|
|
36
|
+
if (!p.isCancel(value))
|
|
37
|
+
return false;
|
|
38
|
+
const confirmExit = await p.confirm({
|
|
39
|
+
message: "Exit the wizard? No changes will be saved.",
|
|
40
|
+
initialValue: false,
|
|
41
|
+
});
|
|
42
|
+
// Only exit when the user explicitly confirms "Yes".
|
|
43
|
+
// Pressing Escape on the confirmation (isCancel) or choosing "No"
|
|
44
|
+
// both mean "stay in the wizard".
|
|
45
|
+
if (confirmExit === true) {
|
|
46
|
+
bail();
|
|
47
|
+
}
|
|
48
|
+
// User chose to stay
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run a prompt function in a loop, retrying if the user presses Escape
|
|
53
|
+
* but decides to stay. Returns the non-cancelled result.
|
|
54
|
+
*/
|
|
55
|
+
async function prompt(fn) {
|
|
56
|
+
for (;;) {
|
|
57
|
+
const result = await fn();
|
|
58
|
+
if (await onCancel(result))
|
|
59
|
+
continue;
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Like `prompt`, but pressing Escape returns `null` instead of re-prompting.
|
|
65
|
+
* Use inside sub-actions so the user can back out to the parent menu.
|
|
66
|
+
*/
|
|
67
|
+
async function promptOrBack(fn) {
|
|
68
|
+
const result = await fn();
|
|
69
|
+
if (p.isCancel(result))
|
|
70
|
+
return null;
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
// ── Steps ───────────────────────────────────────────────────────────────────
|
|
74
|
+
async function stepStashDir(current) {
|
|
75
|
+
const defaultDir = current.stashDir ?? getDefaultStashDir();
|
|
76
|
+
const choice = await prompt(() => p.select({
|
|
77
|
+
message: "Where should akm store skills, commands, and other assets?",
|
|
78
|
+
options: [
|
|
79
|
+
{ value: "default", label: defaultDir, hint: current.stashDir ? "current" : "default" },
|
|
80
|
+
{ value: "custom", label: "Enter a custom path..." },
|
|
81
|
+
],
|
|
82
|
+
}));
|
|
83
|
+
if (choice === "default")
|
|
84
|
+
return defaultDir;
|
|
85
|
+
const customPath = await prompt(() => p.text({
|
|
86
|
+
message: "Enter the stash directory path:",
|
|
87
|
+
placeholder: defaultDir,
|
|
88
|
+
validate: (v) => {
|
|
89
|
+
if (!v?.trim())
|
|
90
|
+
return "Path cannot be empty";
|
|
91
|
+
},
|
|
92
|
+
}));
|
|
93
|
+
return customPath.trim();
|
|
94
|
+
}
|
|
95
|
+
async function stepOllama(current) {
|
|
96
|
+
const spin = p.spinner();
|
|
97
|
+
spin.start("Checking for Ollama...");
|
|
98
|
+
const ollama = await detectOllama();
|
|
99
|
+
if (!ollama.available) {
|
|
100
|
+
spin.stop("Ollama not detected");
|
|
101
|
+
p.log.info("Ollama is not running. Embeddings will use the built-in local model.\n" +
|
|
102
|
+
"To use Ollama later, install it from https://ollama.com and re-run `akm setup`.");
|
|
103
|
+
// Preserve existing embedding/LLM config when Ollama is not available
|
|
104
|
+
return { embedding: current.embedding, llm: current.llm };
|
|
105
|
+
}
|
|
106
|
+
spin.stop(`Ollama detected at ${ollama.endpoint}`);
|
|
107
|
+
if (ollama.models.length > 0) {
|
|
108
|
+
p.log.info(`Available models: ${ollama.models.join(", ")}`);
|
|
109
|
+
}
|
|
110
|
+
// Embedding model selection
|
|
111
|
+
const embeddingModels = ollama.models.filter((m) => m.includes("embed") || m.includes("nomic") || m.includes("minilm") || m.includes("bge"));
|
|
112
|
+
const hasEmbeddingModels = embeddingModels.length > 0;
|
|
113
|
+
let embedding;
|
|
114
|
+
const embeddingOptions = [];
|
|
115
|
+
for (const m of embeddingModels) {
|
|
116
|
+
embeddingOptions.push({ value: m, label: m, hint: "Ollama" });
|
|
117
|
+
}
|
|
118
|
+
embeddingOptions.push({
|
|
119
|
+
value: "local",
|
|
120
|
+
label: "Built-in local embeddings",
|
|
121
|
+
hint: "no server needed",
|
|
122
|
+
});
|
|
123
|
+
if (current.embedding) {
|
|
124
|
+
embeddingOptions.push({
|
|
125
|
+
value: "keep",
|
|
126
|
+
label: `Keep current: ${current.embedding.provider ?? current.embedding.endpoint}`,
|
|
127
|
+
hint: current.embedding.model,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const embChoice = await prompt(() => p.select({
|
|
131
|
+
message: "Which embedding provider should akm use?",
|
|
132
|
+
options: embeddingOptions,
|
|
133
|
+
initialValue: hasEmbeddingModels ? embeddingModels[0] : "local",
|
|
134
|
+
}));
|
|
135
|
+
if (embChoice === "keep") {
|
|
136
|
+
embedding = current.embedding;
|
|
137
|
+
}
|
|
138
|
+
else if (embChoice !== "local") {
|
|
139
|
+
// Ask for dimension — different models produce different sizes
|
|
140
|
+
const dimChoice = await prompt(() => p.text({
|
|
141
|
+
message: "Embedding dimension (must match your index; 384 is common for MiniLM/nomic):",
|
|
142
|
+
placeholder: "384",
|
|
143
|
+
defaultValue: "384",
|
|
144
|
+
validate: (v) => {
|
|
145
|
+
const n = Number(v);
|
|
146
|
+
if (!Number.isInteger(n) || n <= 0)
|
|
147
|
+
return "Must be a positive integer";
|
|
148
|
+
},
|
|
149
|
+
}));
|
|
150
|
+
embedding = {
|
|
151
|
+
provider: "ollama",
|
|
152
|
+
endpoint: `${ollama.endpoint}/v1/embeddings`,
|
|
153
|
+
model: embChoice,
|
|
154
|
+
dimension: Number(dimChoice),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// else: undefined → use built-in local
|
|
158
|
+
// LLM model selection
|
|
159
|
+
const chatModels = ollama.models.filter((m) => !embeddingModels.includes(m));
|
|
160
|
+
const allLlmCandidates = chatModels.length > 0 ? chatModels : ollama.models;
|
|
161
|
+
let llm;
|
|
162
|
+
const llmOptions = [];
|
|
163
|
+
for (const m of allLlmCandidates) {
|
|
164
|
+
llmOptions.push({ value: m, label: m, hint: "Ollama" });
|
|
165
|
+
}
|
|
166
|
+
llmOptions.push({
|
|
167
|
+
value: "none",
|
|
168
|
+
label: "Skip LLM enhancement",
|
|
169
|
+
hint: "use heuristic metadata",
|
|
170
|
+
});
|
|
171
|
+
if (current.llm) {
|
|
172
|
+
llmOptions.push({
|
|
173
|
+
value: "keep",
|
|
174
|
+
label: `Keep current: ${current.llm.provider ?? current.llm.endpoint}`,
|
|
175
|
+
hint: current.llm.model,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const llmChoice = await prompt(() => p.select({
|
|
179
|
+
message: "Use an LLM for richer metadata during indexing?",
|
|
180
|
+
options: llmOptions,
|
|
181
|
+
initialValue: allLlmCandidates.length > 0 ? allLlmCandidates[0] : "none",
|
|
182
|
+
}));
|
|
183
|
+
if (llmChoice === "keep") {
|
|
184
|
+
llm = current.llm;
|
|
185
|
+
}
|
|
186
|
+
else if (llmChoice !== "none") {
|
|
187
|
+
llm = {
|
|
188
|
+
provider: "ollama",
|
|
189
|
+
endpoint: `${ollama.endpoint}/v1/chat/completions`,
|
|
190
|
+
model: llmChoice,
|
|
191
|
+
temperature: 0.3,
|
|
192
|
+
maxTokens: 512,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return { embedding, llm };
|
|
196
|
+
}
|
|
197
|
+
async function stepRegistries(current) {
|
|
198
|
+
const defaults = DEFAULT_CONFIG.registries ?? [];
|
|
199
|
+
const currentRegistries = current.registries ?? defaults;
|
|
200
|
+
const defaultUrls = new Set(defaults.map((r) => r.url));
|
|
201
|
+
const enabledUrls = new Set(currentRegistries.filter((r) => r.enabled !== false).map((r) => r.url));
|
|
202
|
+
// Collect custom (non-default) registries to preserve them
|
|
203
|
+
const customRegistries = currentRegistries.filter((r) => !defaultUrls.has(r.url));
|
|
204
|
+
// Show default registries for toggling
|
|
205
|
+
const options = defaults.map((r) => ({
|
|
206
|
+
value: r.url,
|
|
207
|
+
label: r.name ?? r.url,
|
|
208
|
+
hint: r.provider ?? "static index",
|
|
209
|
+
}));
|
|
210
|
+
if (customRegistries.length > 0) {
|
|
211
|
+
p.log.info(`You have ${customRegistries.length} custom registr${customRegistries.length === 1 ? "y" : "ies"} that will be preserved.`);
|
|
212
|
+
}
|
|
213
|
+
const selected = await prompt(() => p.multiselect({
|
|
214
|
+
message: "Which built-in registries should be enabled?",
|
|
215
|
+
options,
|
|
216
|
+
initialValues: options.filter((o) => enabledUrls.has(o.value)).map((o) => o.value),
|
|
217
|
+
}));
|
|
218
|
+
// If all defaults are selected and there are no custom registries,
|
|
219
|
+
// return undefined to use the built-in defaults (avoids pinning)
|
|
220
|
+
const allDefaultsSelected = defaults.every((r) => selected.includes(r.url));
|
|
221
|
+
if (allDefaultsSelected && customRegistries.length === 0) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
// Build explicit list: toggled defaults + preserved custom registries
|
|
225
|
+
const result = defaults.map((r) => ({
|
|
226
|
+
...r,
|
|
227
|
+
enabled: selected.includes(r.url),
|
|
228
|
+
}));
|
|
229
|
+
// Re-add custom registries unchanged
|
|
230
|
+
for (const custom of customRegistries) {
|
|
231
|
+
result.push(custom);
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* @internal Exported for testing only.
|
|
237
|
+
*/
|
|
238
|
+
export async function stepStashSources(current) {
|
|
239
|
+
const stashes = [...(current.stashes ?? [])];
|
|
240
|
+
if (stashes.length > 0) {
|
|
241
|
+
p.log.info(`You have ${stashes.length} existing stash source(s).`);
|
|
242
|
+
}
|
|
243
|
+
// ── Recommended GitHub repos ───────────────────────────────────────────
|
|
244
|
+
const existingUrls = new Set(stashes.map((s) => s.url));
|
|
245
|
+
const repoOptions = RECOMMENDED_GITHUB_REPOS.map((r) => ({
|
|
246
|
+
value: r.url,
|
|
247
|
+
label: r.name,
|
|
248
|
+
hint: existingUrls.has(r.url) ? `${r.hint} (already added)` : r.hint,
|
|
249
|
+
}));
|
|
250
|
+
const selectedRepos = await prompt(() => p.multiselect({
|
|
251
|
+
message: "Recommended GitHub repositories — toggle to add or remove:",
|
|
252
|
+
options: repoOptions,
|
|
253
|
+
initialValues: repoOptions.filter((o) => existingUrls.has(o.value)).map((o) => o.value),
|
|
254
|
+
required: false,
|
|
255
|
+
}));
|
|
256
|
+
// Add newly selected repos
|
|
257
|
+
for (const url of selectedRepos) {
|
|
258
|
+
if (!existingUrls.has(url)) {
|
|
259
|
+
const rec = RECOMMENDED_GITHUB_REPOS.find((r) => r.url === url);
|
|
260
|
+
stashes.push({ type: "github", url, name: rec?.name });
|
|
261
|
+
existingUrls.add(url);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Remove deselected repos that were previously configured
|
|
265
|
+
for (const rec of RECOMMENDED_GITHUB_REPOS) {
|
|
266
|
+
if (existingUrls.has(rec.url) && !selectedRepos.includes(rec.url)) {
|
|
267
|
+
const idx = stashes.findIndex((s) => s.url === rec.url);
|
|
268
|
+
if (idx !== -1) {
|
|
269
|
+
stashes.splice(idx, 1);
|
|
270
|
+
existingUrls.delete(rec.url);
|
|
271
|
+
p.log.info(`Removed ${rec.name}.`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// ── Additional stash sources loop ──────────────────────────────────────
|
|
276
|
+
let addMore = true;
|
|
277
|
+
while (addMore) {
|
|
278
|
+
const action = await prompt(() => p.select({
|
|
279
|
+
message: "Add another stash source?",
|
|
280
|
+
options: [
|
|
281
|
+
{ value: "openviking", label: "OpenViking server", hint: "remote stash" },
|
|
282
|
+
{ value: "github-repo", label: "GitHub repository", hint: "custom URL" },
|
|
283
|
+
{ value: "filesystem", label: "Filesystem path", hint: "local directory" },
|
|
284
|
+
{ value: "done", label: "Done — no more sources" },
|
|
285
|
+
],
|
|
286
|
+
}));
|
|
287
|
+
if (action === "done") {
|
|
288
|
+
addMore = false;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
if (action === "openviking") {
|
|
292
|
+
const url = await promptOrBack(() => p.text({
|
|
293
|
+
message: "Enter the OpenViking server URL:",
|
|
294
|
+
placeholder: "https://your-openviking-server.example.com",
|
|
295
|
+
validate: (v) => {
|
|
296
|
+
if (!v?.trim())
|
|
297
|
+
return "URL cannot be empty";
|
|
298
|
+
if (!v.startsWith("http://") && !v.startsWith("https://"))
|
|
299
|
+
return "URL must start with http:// or https://";
|
|
300
|
+
},
|
|
301
|
+
}));
|
|
302
|
+
if (url === null)
|
|
303
|
+
continue;
|
|
304
|
+
const spin = p.spinner();
|
|
305
|
+
spin.start("Checking OpenViking server...");
|
|
306
|
+
const result = await detectOpenViking(url.trim());
|
|
307
|
+
if (result.available) {
|
|
308
|
+
spin.stop("Server is reachable");
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
spin.stop("Server not reachable — adding anyway (it may be temporarily down)");
|
|
312
|
+
}
|
|
313
|
+
const name = await promptOrBack(() => p.text({
|
|
314
|
+
message: "Give this stash a name (optional):",
|
|
315
|
+
placeholder: "my-openviking",
|
|
316
|
+
}));
|
|
317
|
+
if (name === null)
|
|
318
|
+
continue;
|
|
319
|
+
// Use the normalized URL from detection (trailing slashes stripped)
|
|
320
|
+
const entry = { type: "openviking", url: result.url };
|
|
321
|
+
if (name.trim())
|
|
322
|
+
entry.name = name.trim();
|
|
323
|
+
if (!stashes.some((s) => s.url === entry.url)) {
|
|
324
|
+
stashes.push(entry);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
p.log.warn("This URL is already configured.");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (action === "github-repo") {
|
|
331
|
+
const url = await promptOrBack(() => p.text({
|
|
332
|
+
message: "Enter the GitHub repository URL:",
|
|
333
|
+
placeholder: "https://github.com/owner/repo",
|
|
334
|
+
validate: (v) => {
|
|
335
|
+
if (!v?.trim())
|
|
336
|
+
return "URL cannot be empty";
|
|
337
|
+
},
|
|
338
|
+
}));
|
|
339
|
+
if (url === null)
|
|
340
|
+
continue;
|
|
341
|
+
const name = await promptOrBack(() => p.text({
|
|
342
|
+
message: "Give this stash a name (optional):",
|
|
343
|
+
placeholder: "my-repo",
|
|
344
|
+
}));
|
|
345
|
+
if (name === null)
|
|
346
|
+
continue;
|
|
347
|
+
const entry = { type: "github", url: url.trim() };
|
|
348
|
+
if (name.trim())
|
|
349
|
+
entry.name = name.trim();
|
|
350
|
+
if (!stashes.some((s) => s.url === entry.url)) {
|
|
351
|
+
stashes.push(entry);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
p.log.warn("This URL is already configured.");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (action === "filesystem") {
|
|
358
|
+
const fsPath = await promptOrBack(() => p.text({
|
|
359
|
+
message: "Enter the directory path:",
|
|
360
|
+
placeholder: "/path/to/stash",
|
|
361
|
+
validate: (v) => {
|
|
362
|
+
if (!v?.trim())
|
|
363
|
+
return "Path cannot be empty";
|
|
364
|
+
},
|
|
365
|
+
}));
|
|
366
|
+
if (fsPath === null)
|
|
367
|
+
continue;
|
|
368
|
+
const resolved = fsPath.trim();
|
|
369
|
+
const name = await promptOrBack(() => p.text({
|
|
370
|
+
message: "Give this stash a name (optional):",
|
|
371
|
+
placeholder: "my-stash",
|
|
372
|
+
}));
|
|
373
|
+
if (name === null)
|
|
374
|
+
continue;
|
|
375
|
+
const entry = { type: "filesystem", path: resolved };
|
|
376
|
+
if (name.trim())
|
|
377
|
+
entry.name = name.trim();
|
|
378
|
+
if (!stashes.some((s) => s.path === entry.path)) {
|
|
379
|
+
stashes.push(entry);
|
|
380
|
+
}
|
|
381
|
+
else {
|
|
382
|
+
p.log.warn("This path is already configured.");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return stashes;
|
|
387
|
+
}
|
|
388
|
+
async function stepAgentPlatforms(current) {
|
|
389
|
+
const platforms = detectAgentPlatforms();
|
|
390
|
+
if (platforms.length === 0) {
|
|
391
|
+
p.log.info("No agent platform configurations detected.");
|
|
392
|
+
return [];
|
|
393
|
+
}
|
|
394
|
+
const existingPaths = new Set((current.stashes ?? []).map((s) => s.path));
|
|
395
|
+
// Filter out platforms already configured
|
|
396
|
+
const newPlatforms = platforms.filter((pl) => !existingPaths.has(pl.path));
|
|
397
|
+
if (newPlatforms.length === 0) {
|
|
398
|
+
p.log.info(`Detected ${platforms.length} agent platform(s), all already configured as stash sources.`);
|
|
399
|
+
return [];
|
|
400
|
+
}
|
|
401
|
+
const selected = await prompt(() => p.multiselect({
|
|
402
|
+
message: "Found agent platform configurations. Add as stash sources?",
|
|
403
|
+
options: newPlatforms.map((pl) => ({
|
|
404
|
+
value: pl.path,
|
|
405
|
+
label: pl.name,
|
|
406
|
+
hint: pl.path,
|
|
407
|
+
})),
|
|
408
|
+
required: false,
|
|
409
|
+
}));
|
|
410
|
+
const entries = [];
|
|
411
|
+
for (const selectedPath of selected) {
|
|
412
|
+
const platform = newPlatforms.find((pl) => pl.path === selectedPath);
|
|
413
|
+
if (platform) {
|
|
414
|
+
entries.push({
|
|
415
|
+
type: "filesystem",
|
|
416
|
+
path: platform.path,
|
|
417
|
+
name: platform.name.toLowerCase().replace(/\s+/g, "-"),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return entries;
|
|
422
|
+
}
|
|
423
|
+
// ── Main Wizard ─────────────────────────────────────────────────────────────
|
|
424
|
+
export async function runSetupWizard() {
|
|
425
|
+
p.intro("akm setup");
|
|
426
|
+
const current = loadConfig();
|
|
427
|
+
const configPath = getConfigPath();
|
|
428
|
+
// Step 1: Stash directory
|
|
429
|
+
p.log.step("Step 1: Stash Directory");
|
|
430
|
+
const stashDir = await stepStashDir(current);
|
|
431
|
+
// Step 2: Ollama / Embedding / LLM
|
|
432
|
+
p.log.step("Step 2: Embedding & LLM");
|
|
433
|
+
const { embedding, llm } = await stepOllama(current);
|
|
434
|
+
// Step 3: Registries
|
|
435
|
+
p.log.step("Step 3: Registries");
|
|
436
|
+
const registries = await stepRegistries(current);
|
|
437
|
+
// Step 4: Stash sources
|
|
438
|
+
p.log.step("Step 4: Stash Sources");
|
|
439
|
+
const stashes = await stepStashSources(current);
|
|
440
|
+
// Step 5: Agent platform detection
|
|
441
|
+
p.log.step("Step 5: Agent Platform Detection");
|
|
442
|
+
const platformStashes = await stepAgentPlatforms(current);
|
|
443
|
+
// Merge platform stashes into main stashes list
|
|
444
|
+
const allStashes = [...stashes];
|
|
445
|
+
for (const ps of platformStashes) {
|
|
446
|
+
if (!allStashes.some((s) => s.path === ps.path)) {
|
|
447
|
+
allStashes.push(ps);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Build final config
|
|
451
|
+
const newConfig = {
|
|
452
|
+
...current,
|
|
453
|
+
stashDir,
|
|
454
|
+
embedding,
|
|
455
|
+
llm,
|
|
456
|
+
registries,
|
|
457
|
+
stashes: allStashes.length > 0 ? allStashes : undefined,
|
|
458
|
+
// Preserve existing fields
|
|
459
|
+
semanticSearch: current.semanticSearch,
|
|
460
|
+
installed: current.installed,
|
|
461
|
+
output: current.output,
|
|
462
|
+
};
|
|
463
|
+
// Confirm before saving
|
|
464
|
+
const effectiveRegistries = registries ?? DEFAULT_CONFIG.registries ?? [];
|
|
465
|
+
p.note([
|
|
466
|
+
`Stash directory: ${stashDir}`,
|
|
467
|
+
`Embedding: ${embedding ? `${embedding.provider ?? "remote"} / ${embedding.model}` : "built-in local"}`,
|
|
468
|
+
`LLM: ${llm ? `${llm.provider ?? "remote"} / ${llm.model}` : "disabled"}`,
|
|
469
|
+
`Registries: ${effectiveRegistries.filter((r) => r.enabled !== false).length} enabled`,
|
|
470
|
+
`Stash sources: ${allStashes.length}`,
|
|
471
|
+
].join("\n"), "Configuration Summary");
|
|
472
|
+
const shouldSave = await prompt(() => p.confirm({
|
|
473
|
+
message: "Save this configuration?",
|
|
474
|
+
initialValue: true,
|
|
475
|
+
}));
|
|
476
|
+
if (!shouldSave)
|
|
477
|
+
bail();
|
|
478
|
+
// Save config
|
|
479
|
+
saveConfig(newConfig);
|
|
480
|
+
// Initialize stash directory
|
|
481
|
+
await akmInit({ dir: stashDir });
|
|
482
|
+
// Build search index
|
|
483
|
+
const spin = p.spinner();
|
|
484
|
+
spin.start("Building search index...");
|
|
485
|
+
try {
|
|
486
|
+
const indexResult = await akmIndex({ stashDir });
|
|
487
|
+
spin.stop(`Indexed ${indexResult.totalEntries} assets.`);
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
spin.stop("Indexing failed — you can run `akm index` manually later.");
|
|
491
|
+
p.log.warn(String(err));
|
|
492
|
+
}
|
|
493
|
+
// API key reminder
|
|
494
|
+
if (embedding?.apiKey === undefined && embedding?.provider !== "ollama") {
|
|
495
|
+
// Only remind about API keys for non-Ollama remote providers
|
|
496
|
+
if (embedding?.endpoint && !embedding.endpoint.includes("localhost")) {
|
|
497
|
+
p.log.info("Reminder: Set your embedding API key via the AKM_EMBED_API_KEY environment variable.");
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (llm?.apiKey === undefined && llm?.provider !== "ollama") {
|
|
501
|
+
if (llm?.endpoint && !llm.endpoint.includes("localhost")) {
|
|
502
|
+
p.log.info("Reminder: Set your LLM API key via the AKM_LLM_API_KEY environment variable.");
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
p.outro(`Configuration saved to ${configPath}`);
|
|
506
|
+
}
|
|
@@ -80,6 +80,7 @@ class ContextHubStashProvider {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
registerStashProvider("context-hub", (config) => new ContextHubStashProvider(config));
|
|
83
|
+
registerStashProvider("github", (config) => new ContextHubStashProvider(config));
|
|
83
84
|
function getCachePaths(repoUrl) {
|
|
84
85
|
const key = createHash("sha256").update(repoUrl).digest("hex").slice(0, 16);
|
|
85
86
|
const rootDir = path.join(getRegistryIndexCacheDir(), `context-hub-${key}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "akm-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI tool to search, open, and run extension assets from an akm stash directory.",
|
|
6
6
|
"keywords": [
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"bun": ">=1.0.0"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
+
"@clack/prompts": "^1.1.0",
|
|
61
62
|
"citty": "^0.2.1"
|
|
62
63
|
}
|
|
63
64
|
}
|