akm-cli 0.4.0 → 0.5.0-rc1
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/dist/asset-registry.js +7 -0
- package/dist/asset-spec.js +35 -0
- package/dist/cli.js +1383 -25
- package/dist/completions.js +2 -2
- package/dist/config-cli.js +41 -0
- package/dist/config.js +62 -0
- package/dist/embedder.js +26 -4
- package/dist/file-context.js +2 -1
- package/dist/github.js +20 -1
- package/dist/indexer.js +55 -5
- package/dist/init.js +11 -0
- package/dist/install-audit.js +53 -8
- package/dist/installed-kits.js +2 -0
- package/dist/llm.js +64 -23
- package/dist/matchers.js +56 -4
- package/dist/metadata.js +68 -4
- package/dist/paths.js +3 -0
- package/dist/registry-install.js +36 -7
- package/dist/registry-resolve.js +25 -0
- package/dist/renderers.js +182 -2
- package/dist/search-fields.js +4 -0
- package/dist/search-source.js +12 -8
- package/dist/setup.js +158 -33
- package/dist/stash-add.js +84 -11
- package/dist/stash-providers/git.js +182 -44
- package/dist/stash-show.js +56 -1
- package/dist/stash-source-manage.js +14 -4
- package/dist/templates/wiki-templates.js +100 -0
- package/dist/vault.js +290 -0
- package/dist/wiki.js +886 -0
- package/dist/workflow-authoring.js +131 -0
- package/dist/workflow-cli.js +44 -0
- package/dist/workflow-db.js +55 -0
- package/dist/workflow-markdown.js +251 -0
- package/dist/workflow-runs.js +364 -0
- package/package.json +2 -1
- package/LICENSE +0 -374
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { defineCommand, runMain } from "citty";
|
|
5
|
-
import {
|
|
5
|
+
import { deriveCanonicalAssetName, resolveAssetPathFromName } from "./asset-spec";
|
|
6
|
+
import { isWithin, resolveStashDir } from "./common";
|
|
6
7
|
import { generateBashCompletions, installBashCompletions } from "./completions";
|
|
7
8
|
import { DEFAULT_CONFIG, getConfigPath, loadConfig, loadUserConfig, saveConfig } from "./config";
|
|
8
9
|
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./config-cli";
|
|
@@ -19,17 +20,26 @@ import { searchRegistry } from "./registry-search";
|
|
|
19
20
|
import { checkForUpdate, performUpgrade } from "./self-update";
|
|
20
21
|
import { akmAdd } from "./stash-add";
|
|
21
22
|
import { akmClone } from "./stash-clone";
|
|
23
|
+
import { saveGitStash } from "./stash-providers/git";
|
|
24
|
+
import { parseAssetRef } from "./stash-ref";
|
|
22
25
|
import { akmSearch, parseSearchSource } from "./stash-search";
|
|
23
26
|
import { akmShowUnified } from "./stash-show";
|
|
24
27
|
import { addStash } from "./stash-source-manage";
|
|
25
28
|
import { insertUsageEvent } from "./usage-events";
|
|
26
29
|
import { pkgVersion } from "./version";
|
|
27
30
|
import { setQuiet, warn } from "./warn";
|
|
31
|
+
import { createWorkflowAsset, getWorkflowTemplate } from "./workflow-authoring";
|
|
32
|
+
import { hasWorkflowSubcommand, parseWorkflowJsonObject, parseWorkflowStepState, WORKFLOW_STEP_STATES, } from "./workflow-cli";
|
|
33
|
+
import { completeWorkflowStep, getNextWorkflowStep, getWorkflowStatus, listWorkflowRuns, resumeWorkflowRun, startWorkflowRun, } from "./workflow-runs";
|
|
28
34
|
const OUTPUT_FORMATS = ["json", "yaml", "text", "jsonl"];
|
|
29
35
|
const DETAIL_LEVELS = ["brief", "normal", "full", "summary"];
|
|
30
36
|
const NORMAL_DESCRIPTION_LIMIT = 250;
|
|
37
|
+
const MAX_CAPTURED_ASSET_SLUG_LENGTH = 64;
|
|
31
38
|
const CONTEXT_HUB_ALIAS_REF = "context-hub";
|
|
32
39
|
const CONTEXT_HUB_ALIAS_URL = "https://github.com/andrewyng/context-hub";
|
|
40
|
+
const SKILLS_SH_NAME = "skills.sh";
|
|
41
|
+
const SKILLS_SH_URL = "https://skills.sh";
|
|
42
|
+
const SKILLS_SH_PROVIDER = "skills-sh";
|
|
33
43
|
import { stringify as yamlStringify } from "yaml";
|
|
34
44
|
function parseOutputFormat(value) {
|
|
35
45
|
if (!value)
|
|
@@ -230,10 +240,27 @@ function shapeShowOutput(result, detail, forAgent = false) {
|
|
|
230
240
|
"modelHint",
|
|
231
241
|
"agent",
|
|
232
242
|
"parameters",
|
|
243
|
+
"workflowTitle",
|
|
244
|
+
"workflowParameters",
|
|
245
|
+
"steps",
|
|
246
|
+
"keys",
|
|
247
|
+
"comments",
|
|
233
248
|
]);
|
|
234
249
|
}
|
|
235
250
|
if (detail === "summary") {
|
|
236
|
-
return pickFields(result, [
|
|
251
|
+
return pickFields(result, [
|
|
252
|
+
"type",
|
|
253
|
+
"name",
|
|
254
|
+
"description",
|
|
255
|
+
"tags",
|
|
256
|
+
"parameters",
|
|
257
|
+
"workflowTitle",
|
|
258
|
+
"action",
|
|
259
|
+
"run",
|
|
260
|
+
"origin",
|
|
261
|
+
"keys",
|
|
262
|
+
"comments",
|
|
263
|
+
]);
|
|
237
264
|
}
|
|
238
265
|
const base = pickFields(result, [
|
|
239
266
|
"type",
|
|
@@ -249,9 +276,14 @@ function shapeShowOutput(result, detail, forAgent = false) {
|
|
|
249
276
|
"modelHint",
|
|
250
277
|
"agent",
|
|
251
278
|
"parameters",
|
|
279
|
+
"workflowTitle",
|
|
280
|
+
"workflowParameters",
|
|
281
|
+
"steps",
|
|
252
282
|
"run",
|
|
253
283
|
"setup",
|
|
254
284
|
"cwd",
|
|
285
|
+
"keys",
|
|
286
|
+
"comments",
|
|
255
287
|
]);
|
|
256
288
|
if (detail !== "full") {
|
|
257
289
|
return base;
|
|
@@ -304,10 +336,22 @@ function formatPlain(command, result, detail) {
|
|
|
304
336
|
lines.push(`# ${String(r.action)}`);
|
|
305
337
|
if (r.description)
|
|
306
338
|
lines.push(`description: ${String(r.description)}`);
|
|
339
|
+
if (r.workflowTitle)
|
|
340
|
+
lines.push(`workflowTitle: ${String(r.workflowTitle)}`);
|
|
307
341
|
if (r.agent)
|
|
308
342
|
lines.push(`agent: ${String(r.agent)}`);
|
|
309
343
|
if (Array.isArray(r.parameters) && r.parameters.length > 0)
|
|
310
344
|
lines.push(`parameters: ${r.parameters.join(", ")}`);
|
|
345
|
+
if (Array.isArray(r.workflowParameters) && r.workflowParameters.length > 0) {
|
|
346
|
+
lines.push("workflowParameters:");
|
|
347
|
+
for (const parameter of r.workflowParameters) {
|
|
348
|
+
const name = typeof parameter.name === "string" ? parameter.name : "unknown";
|
|
349
|
+
const description = typeof parameter.description === "string" && parameter.description.trim()
|
|
350
|
+
? `: ${parameter.description}`
|
|
351
|
+
: "";
|
|
352
|
+
lines.push(` - ${name}${description}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
311
355
|
if (r.modelHint != null)
|
|
312
356
|
lines.push(`modelHint: ${String(r.modelHint)}`);
|
|
313
357
|
if (r.toolPolicy != null)
|
|
@@ -329,6 +373,25 @@ function formatPlain(command, result, detail) {
|
|
|
329
373
|
lines.push(`schemaVersion: ${String(r.schemaVersion)}`);
|
|
330
374
|
}
|
|
331
375
|
const payloads = [r.content, r.template, r.prompt].filter((value) => value != null).map(String);
|
|
376
|
+
if (Array.isArray(r.steps) && r.steps.length > 0) {
|
|
377
|
+
if (lines.length > 0)
|
|
378
|
+
lines.push("");
|
|
379
|
+
lines.push("steps:");
|
|
380
|
+
for (const [index, step] of r.steps.entries()) {
|
|
381
|
+
const title = typeof step.title === "string" ? step.title : "Untitled step";
|
|
382
|
+
const id = typeof step.id === "string" ? step.id : "unknown";
|
|
383
|
+
lines.push(` ${index + 1}. ${title} [${id}]`);
|
|
384
|
+
if (typeof step.instructions === "string" && step.instructions.trim()) {
|
|
385
|
+
lines.push(` instructions: ${step.instructions.replace(/\n+/g, " ").trim()}`);
|
|
386
|
+
}
|
|
387
|
+
if (Array.isArray(step.completionCriteria) && step.completionCriteria.length > 0) {
|
|
388
|
+
lines.push(" completion:");
|
|
389
|
+
for (const criterion of step.completionCriteria) {
|
|
390
|
+
lines.push(` - ${String(criterion)}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
332
395
|
if (payloads.length > 0) {
|
|
333
396
|
if (lines.length > 0)
|
|
334
397
|
lines.push("");
|
|
@@ -342,6 +405,47 @@ function formatPlain(command, result, detail) {
|
|
|
342
405
|
case "curate": {
|
|
343
406
|
return formatCuratePlain(r, detail);
|
|
344
407
|
}
|
|
408
|
+
case "wiki-list": {
|
|
409
|
+
return formatWikiListPlain(r);
|
|
410
|
+
}
|
|
411
|
+
case "wiki-show": {
|
|
412
|
+
return formatWikiShowPlain(r);
|
|
413
|
+
}
|
|
414
|
+
case "wiki-create": {
|
|
415
|
+
return formatWikiCreatePlain(r);
|
|
416
|
+
}
|
|
417
|
+
case "wiki-remove": {
|
|
418
|
+
return formatWikiRemovePlain(r);
|
|
419
|
+
}
|
|
420
|
+
case "wiki-pages": {
|
|
421
|
+
return formatWikiPagesPlain(r);
|
|
422
|
+
}
|
|
423
|
+
case "wiki-stash": {
|
|
424
|
+
return formatWikiStashPlain(r);
|
|
425
|
+
}
|
|
426
|
+
case "wiki-lint": {
|
|
427
|
+
return formatWikiLintPlain(r);
|
|
428
|
+
}
|
|
429
|
+
case "wiki-ingest": {
|
|
430
|
+
return formatWikiIngestPlain(r);
|
|
431
|
+
}
|
|
432
|
+
case "workflow-start":
|
|
433
|
+
case "workflow-status":
|
|
434
|
+
case "workflow-complete": {
|
|
435
|
+
return formatWorkflowStatusPlain(r);
|
|
436
|
+
}
|
|
437
|
+
case "workflow-next": {
|
|
438
|
+
return formatWorkflowNextPlain(r);
|
|
439
|
+
}
|
|
440
|
+
case "workflow-list": {
|
|
441
|
+
return formatWorkflowListPlain(r);
|
|
442
|
+
}
|
|
443
|
+
case "workflow-create": {
|
|
444
|
+
if (r.ref && r.path) {
|
|
445
|
+
return `Created ${String(r.ref)} at ${String(r.path)}`;
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
345
449
|
case "list": {
|
|
346
450
|
const sources = Array.isArray(r.sources) ? r.sources : [];
|
|
347
451
|
if (sources.length === 0)
|
|
@@ -352,7 +456,13 @@ function formatPlain(command, result, detail) {
|
|
|
352
456
|
const name = typeof src.name === "string" ? src.name : "unnamed";
|
|
353
457
|
const ver = typeof src.version === "string" ? ` v${src.version}` : "";
|
|
354
458
|
const prov = typeof src.provider === "string" ? ` (${src.provider})` : "";
|
|
355
|
-
|
|
459
|
+
const flags = [];
|
|
460
|
+
if (src.updatable === true)
|
|
461
|
+
flags.push("updatable");
|
|
462
|
+
if (src.writable === true)
|
|
463
|
+
flags.push("writable");
|
|
464
|
+
const flagText = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
|
|
465
|
+
lines.push(`[${kind}] ${name}${ver}${prov}${flagText}`);
|
|
356
466
|
}
|
|
357
467
|
return lines.join("\n");
|
|
358
468
|
}
|
|
@@ -414,6 +524,69 @@ function formatPlain(command, result, detail) {
|
|
|
414
524
|
return null; // fall through to YAML
|
|
415
525
|
}
|
|
416
526
|
}
|
|
527
|
+
function formatWorkflowListPlain(result) {
|
|
528
|
+
const runs = Array.isArray(result.runs) ? result.runs : [];
|
|
529
|
+
if (runs.length === 0)
|
|
530
|
+
return "No workflow runs found.";
|
|
531
|
+
return runs
|
|
532
|
+
.map((run) => {
|
|
533
|
+
const id = typeof run.id === "string" ? run.id : "unknown";
|
|
534
|
+
const ref = typeof run.workflowRef === "string" ? run.workflowRef : "workflow:unknown";
|
|
535
|
+
const status = typeof run.status === "string" ? run.status : "unknown";
|
|
536
|
+
const currentStep = typeof run.currentStepId === "string" ? ` (current: ${run.currentStepId})` : "";
|
|
537
|
+
return `${id} ${ref} [${status}]${currentStep}`;
|
|
538
|
+
})
|
|
539
|
+
.join("\n");
|
|
540
|
+
}
|
|
541
|
+
function formatWorkflowStatusPlain(result) {
|
|
542
|
+
const run = typeof result.run === "object" && result.run !== null ? result.run : undefined;
|
|
543
|
+
const workflow = typeof result.workflow === "object" && result.workflow !== null
|
|
544
|
+
? result.workflow
|
|
545
|
+
: undefined;
|
|
546
|
+
if (!run || !workflow)
|
|
547
|
+
return null;
|
|
548
|
+
const lines = [
|
|
549
|
+
`workflow: ${String(workflow.ref ?? "workflow:unknown")}`,
|
|
550
|
+
`run: ${String(run.id ?? "unknown")}`,
|
|
551
|
+
`title: ${String(run.workflowTitle ?? workflow.title ?? "Workflow")}`,
|
|
552
|
+
`status: ${String(run.status ?? "unknown")}`,
|
|
553
|
+
];
|
|
554
|
+
if (run.currentStepId)
|
|
555
|
+
lines.push(`currentStep: ${String(run.currentStepId)}`);
|
|
556
|
+
const steps = Array.isArray(workflow.steps) ? workflow.steps : [];
|
|
557
|
+
if (steps.length > 0) {
|
|
558
|
+
lines.push("steps:");
|
|
559
|
+
for (const step of steps) {
|
|
560
|
+
const title = typeof step.title === "string" ? step.title : "Untitled step";
|
|
561
|
+
const id = typeof step.id === "string" ? step.id : "unknown";
|
|
562
|
+
const status = typeof step.status === "string" ? step.status : "unknown";
|
|
563
|
+
lines.push(` - ${title} [${id}] (${status})`);
|
|
564
|
+
if (typeof step.notes === "string" && step.notes.trim()) {
|
|
565
|
+
lines.push(` notes: ${step.notes}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return lines.join("\n");
|
|
570
|
+
}
|
|
571
|
+
function formatWorkflowNextPlain(result) {
|
|
572
|
+
const base = formatWorkflowStatusPlain(result);
|
|
573
|
+
const step = typeof result.step === "object" && result.step !== null ? result.step : undefined;
|
|
574
|
+
if (!step)
|
|
575
|
+
return base;
|
|
576
|
+
const lines = base ? [base, "", "next:"] : ["next:"];
|
|
577
|
+
lines.push(` ${String(step.title ?? "Untitled step")} [${String(step.id ?? "unknown")}]`);
|
|
578
|
+
if (typeof step.instructions === "string" && step.instructions.trim()) {
|
|
579
|
+
lines.push(` instructions: ${step.instructions.replace(/\n+/g, " ").trim()}`);
|
|
580
|
+
}
|
|
581
|
+
const completion = Array.isArray(step.completionCriteria) ? step.completionCriteria : [];
|
|
582
|
+
if (completion.length > 0) {
|
|
583
|
+
lines.push(" completion:");
|
|
584
|
+
for (const criterion of completion) {
|
|
585
|
+
lines.push(` - ${String(criterion)}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return lines.join("\n");
|
|
589
|
+
}
|
|
417
590
|
function formatSearchPlain(r, detail) {
|
|
418
591
|
const hits = r.hits ?? [];
|
|
419
592
|
const registryHits = r.registryHits ?? [];
|
|
@@ -474,6 +647,98 @@ function formatSearchPlain(r, detail) {
|
|
|
474
647
|
}
|
|
475
648
|
return lines.join("\n").trimEnd();
|
|
476
649
|
}
|
|
650
|
+
function formatWikiListPlain(r) {
|
|
651
|
+
const wikis = Array.isArray(r.wikis) ? r.wikis : [];
|
|
652
|
+
if (wikis.length === 0)
|
|
653
|
+
return "No wikis. Create one with `akm wiki create <name>`.";
|
|
654
|
+
const lines = ["NAME\tPAGES\tRAWS\tLAST-MODIFIED"];
|
|
655
|
+
for (const w of wikis) {
|
|
656
|
+
const name = typeof w.name === "string" ? w.name : "?";
|
|
657
|
+
const pages = typeof w.pages === "number" ? w.pages : 0;
|
|
658
|
+
const raws = typeof w.raws === "number" ? w.raws : 0;
|
|
659
|
+
const modified = typeof w.lastModified === "string" ? w.lastModified : "-";
|
|
660
|
+
lines.push(`${name}\t${pages}\t${raws}\t${modified}`);
|
|
661
|
+
}
|
|
662
|
+
return lines.join("\n");
|
|
663
|
+
}
|
|
664
|
+
function formatWikiShowPlain(r) {
|
|
665
|
+
const lines = [];
|
|
666
|
+
if (r.name)
|
|
667
|
+
lines.push(`# wiki: ${String(r.name)}`);
|
|
668
|
+
if (r.path)
|
|
669
|
+
lines.push(`path: ${String(r.path)}`);
|
|
670
|
+
if (r.description)
|
|
671
|
+
lines.push(`description: ${String(r.description)}`);
|
|
672
|
+
if (typeof r.pages === "number")
|
|
673
|
+
lines.push(`pages: ${r.pages}`);
|
|
674
|
+
if (typeof r.raws === "number")
|
|
675
|
+
lines.push(`raws: ${r.raws}`);
|
|
676
|
+
if (r.lastModified)
|
|
677
|
+
lines.push(`lastModified: ${String(r.lastModified)}`);
|
|
678
|
+
const recentLog = Array.isArray(r.recentLog) ? r.recentLog : [];
|
|
679
|
+
if (recentLog.length > 0) {
|
|
680
|
+
lines.push("", "recent log:");
|
|
681
|
+
for (const entry of recentLog) {
|
|
682
|
+
lines.push(entry);
|
|
683
|
+
lines.push("");
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return lines.join("\n").trimEnd();
|
|
687
|
+
}
|
|
688
|
+
function formatWikiCreatePlain(r) {
|
|
689
|
+
const created = Array.isArray(r.created) ? r.created : [];
|
|
690
|
+
const skipped = Array.isArray(r.skipped) ? r.skipped : [];
|
|
691
|
+
const lines = [`Created wiki ${String(r.ref ?? r.name)} at ${String(r.path ?? "?")}`];
|
|
692
|
+
if (created.length > 0)
|
|
693
|
+
lines.push(` created: ${created.length} file(s)`);
|
|
694
|
+
if (skipped.length > 0)
|
|
695
|
+
lines.push(` skipped: ${skipped.length} existing file(s)`);
|
|
696
|
+
return lines.join("\n");
|
|
697
|
+
}
|
|
698
|
+
function formatWikiRemovePlain(r) {
|
|
699
|
+
const preserved = r.preservedRaw === true;
|
|
700
|
+
const removed = Array.isArray(r.removed) ? r.removed.length : 0;
|
|
701
|
+
const base = `Removed wiki ${String(r.name ?? "?")} (${removed} path(s))`;
|
|
702
|
+
return preserved ? `${base}; preserved ${String(r.rawPath ?? "raw/")}` : base;
|
|
703
|
+
}
|
|
704
|
+
function formatWikiPagesPlain(r) {
|
|
705
|
+
const pages = Array.isArray(r.pages) ? r.pages : [];
|
|
706
|
+
if (pages.length === 0)
|
|
707
|
+
return `No pages in wiki:${String(r.wiki ?? "?")}.`;
|
|
708
|
+
const lines = [];
|
|
709
|
+
for (const p of pages) {
|
|
710
|
+
const ref = String(p.ref ?? "?");
|
|
711
|
+
const kind = typeof p.pageKind === "string" ? ` [${p.pageKind}]` : "";
|
|
712
|
+
const desc = typeof p.description === "string" && p.description ? ` — ${p.description}` : "";
|
|
713
|
+
lines.push(`${ref}${kind}${desc}`);
|
|
714
|
+
}
|
|
715
|
+
return lines.join("\n");
|
|
716
|
+
}
|
|
717
|
+
function formatWikiStashPlain(r) {
|
|
718
|
+
const slug = String(r.slug ?? "?");
|
|
719
|
+
const pathValue = String(r.path ?? "?");
|
|
720
|
+
return `Stashed ${slug} → ${pathValue}`;
|
|
721
|
+
}
|
|
722
|
+
function formatWikiLintPlain(r) {
|
|
723
|
+
const findings = Array.isArray(r.findings) ? r.findings : [];
|
|
724
|
+
const pagesScanned = typeof r.pagesScanned === "number" ? r.pagesScanned : 0;
|
|
725
|
+
const rawsScanned = typeof r.rawsScanned === "number" ? r.rawsScanned : 0;
|
|
726
|
+
const header = `${findings.length} finding(s) in wiki:${String(r.wiki ?? "?")} (${pagesScanned} page(s), ${rawsScanned} raw(s))`;
|
|
727
|
+
if (findings.length === 0)
|
|
728
|
+
return `${header} — clean.`;
|
|
729
|
+
const lines = [header];
|
|
730
|
+
for (const f of findings) {
|
|
731
|
+
const kind = String(f.kind ?? "?");
|
|
732
|
+
const message = String(f.message ?? "");
|
|
733
|
+
lines.push(`- [${kind}] ${message}`);
|
|
734
|
+
}
|
|
735
|
+
return lines.join("\n");
|
|
736
|
+
}
|
|
737
|
+
function formatWikiIngestPlain(r) {
|
|
738
|
+
if (typeof r.workflow === "string")
|
|
739
|
+
return r.workflow;
|
|
740
|
+
return JSON.stringify(r, null, 2);
|
|
741
|
+
}
|
|
477
742
|
function formatCuratePlain(r, detail) {
|
|
478
743
|
const query = typeof r.query === "string" ? r.query : "";
|
|
479
744
|
const summary = typeof r.summary === "string" ? r.summary : "";
|
|
@@ -808,7 +1073,7 @@ const searchCommand = defineCommand({
|
|
|
808
1073
|
query: { type: "positional", description: "Search query (omit to list all assets)", required: false, default: "" },
|
|
809
1074
|
type: {
|
|
810
1075
|
type: "string",
|
|
811
|
-
description: "Asset type filter (
|
|
1076
|
+
description: "Asset type filter (skill, command, agent, knowledge, workflow, script, memory, vault, wiki, or any). Use workflow to find step-by-step task assets.",
|
|
812
1077
|
},
|
|
813
1078
|
limit: { type: "string", description: "Maximum number of results" },
|
|
814
1079
|
source: { type: "string", description: "Search source (stash|registry|both)", default: "stash" },
|
|
@@ -835,7 +1100,7 @@ const curateCommand = defineCommand({
|
|
|
835
1100
|
query: { type: "positional", description: "Task or prompt to curate assets for", required: true },
|
|
836
1101
|
type: {
|
|
837
1102
|
type: "string",
|
|
838
|
-
description: "Asset type filter (
|
|
1103
|
+
description: "Asset type filter (skill, command, agent, knowledge, workflow, script, memory, vault, wiki, or any). Use workflow to curate step-by-step task assets.",
|
|
839
1104
|
},
|
|
840
1105
|
limit: { type: "string", description: "Maximum number of curated results", default: "4" },
|
|
841
1106
|
source: { type: "string", description: "Search source (stash|registry|both)", default: "stash" },
|
|
@@ -876,6 +1141,20 @@ const addCommand = defineCommand({
|
|
|
876
1141
|
provider: { type: "string", description: "Provider type (e.g. openviking). Required for URL sources." },
|
|
877
1142
|
options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
|
|
878
1143
|
name: { type: "string", description: "Human-friendly name for the source" },
|
|
1144
|
+
writable: {
|
|
1145
|
+
type: "boolean",
|
|
1146
|
+
description: "Mark a git stash as writable so changes can be pushed back",
|
|
1147
|
+
default: false,
|
|
1148
|
+
},
|
|
1149
|
+
trust: {
|
|
1150
|
+
type: "boolean",
|
|
1151
|
+
description: "Bypass install-audit blocking for this add invocation only",
|
|
1152
|
+
default: false,
|
|
1153
|
+
},
|
|
1154
|
+
type: {
|
|
1155
|
+
type: "string",
|
|
1156
|
+
description: "Override asset type for all files in this stash (currently supports: wiki)",
|
|
1157
|
+
},
|
|
879
1158
|
"max-pages": { type: "string", description: "Maximum pages to crawl for website sources (default: 50)" },
|
|
880
1159
|
"max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
|
|
881
1160
|
},
|
|
@@ -886,7 +1165,7 @@ const addCommand = defineCommand({
|
|
|
886
1165
|
if (ref === CONTEXT_HUB_ALIAS_REF) {
|
|
887
1166
|
const result = addStash({
|
|
888
1167
|
target: CONTEXT_HUB_ALIAS_URL,
|
|
889
|
-
providerType: "
|
|
1168
|
+
providerType: "git",
|
|
890
1169
|
name: "context-hub",
|
|
891
1170
|
});
|
|
892
1171
|
output("stash-add", result);
|
|
@@ -917,6 +1196,7 @@ const addCommand = defineCommand({
|
|
|
917
1196
|
name: args.name,
|
|
918
1197
|
providerType: args.provider,
|
|
919
1198
|
options: parsedOptions,
|
|
1199
|
+
writable: args.writable,
|
|
920
1200
|
});
|
|
921
1201
|
output("stash-add", result);
|
|
922
1202
|
return;
|
|
@@ -932,7 +1212,10 @@ const addCommand = defineCommand({
|
|
|
932
1212
|
const result = await akmAdd({
|
|
933
1213
|
ref,
|
|
934
1214
|
name: args.name,
|
|
1215
|
+
overrideType: args.type,
|
|
935
1216
|
options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
|
|
1217
|
+
trustThisInstall: args.trust,
|
|
1218
|
+
writable: args.writable,
|
|
936
1219
|
});
|
|
937
1220
|
output("add", result);
|
|
938
1221
|
});
|
|
@@ -1170,6 +1453,91 @@ const configCommand = defineCommand({
|
|
|
1170
1453
|
});
|
|
1171
1454
|
},
|
|
1172
1455
|
});
|
|
1456
|
+
const saveCommand = defineCommand({
|
|
1457
|
+
meta: {
|
|
1458
|
+
name: "save",
|
|
1459
|
+
description: "Save changes in a git-backed stash: commits (and pushes when writable + remote is configured). No-op for non-git stashes.",
|
|
1460
|
+
},
|
|
1461
|
+
args: {
|
|
1462
|
+
name: {
|
|
1463
|
+
type: "positional",
|
|
1464
|
+
description: "Name of the git stash to save (default: primary stash directory)",
|
|
1465
|
+
required: false,
|
|
1466
|
+
},
|
|
1467
|
+
message: {
|
|
1468
|
+
type: "string",
|
|
1469
|
+
alias: "m",
|
|
1470
|
+
description: "Commit message (default: timestamp)",
|
|
1471
|
+
},
|
|
1472
|
+
},
|
|
1473
|
+
async run({ args }) {
|
|
1474
|
+
await runWithJsonErrors(async () => {
|
|
1475
|
+
// Fix: citty can consume `--format json` (space-separated) as the
|
|
1476
|
+
// positional `name` argument (e.g. `akm save --format json` parses
|
|
1477
|
+
// name="json"). Detect the mis-parse by checking argv order — only
|
|
1478
|
+
// treat the positional as consumed by --format when --format appears
|
|
1479
|
+
// before any standalone occurrence of the same value in the save
|
|
1480
|
+
// subcommand's argv slice. This preserves legitimate invocations
|
|
1481
|
+
// like `akm save json --format json`.
|
|
1482
|
+
const parsedFormat = parseFlagValue("--format");
|
|
1483
|
+
const effectiveName = args.name !== undefined &&
|
|
1484
|
+
parsedFormat !== undefined &&
|
|
1485
|
+
args.name === parsedFormat &&
|
|
1486
|
+
wasFormatValueConsumedAsName(args.name, parsedFormat)
|
|
1487
|
+
? undefined
|
|
1488
|
+
: args.name;
|
|
1489
|
+
let writable;
|
|
1490
|
+
if (!effectiveName) {
|
|
1491
|
+
// Primary stash — honour the root-level writable flag from config.
|
|
1492
|
+
const cfg = loadConfig();
|
|
1493
|
+
writable = cfg.writable === true ? true : undefined;
|
|
1494
|
+
}
|
|
1495
|
+
const result = saveGitStash(effectiveName, args.message, writable);
|
|
1496
|
+
output("save", result);
|
|
1497
|
+
});
|
|
1498
|
+
},
|
|
1499
|
+
});
|
|
1500
|
+
/**
|
|
1501
|
+
* Detect whether `--format <value>` was consumed by citty as the optional
|
|
1502
|
+
* `name` positional of `akm save`. Returns true only when `--format` appears
|
|
1503
|
+
* in the save subcommand's argv slice AND the candidate name does NOT
|
|
1504
|
+
* appear as a standalone positional elsewhere (before or after the flag).
|
|
1505
|
+
*
|
|
1506
|
+
* This keeps `akm save json --format json` routing `json` as the stash name,
|
|
1507
|
+
* while `akm save --format json` (no separate positional) is treated as a
|
|
1508
|
+
* primary-stash save.
|
|
1509
|
+
*/
|
|
1510
|
+
function wasFormatValueConsumedAsName(name, formatValue) {
|
|
1511
|
+
const argv = process.argv.slice(2);
|
|
1512
|
+
const saveIndex = argv.indexOf("save");
|
|
1513
|
+
const tokens = saveIndex >= 0 ? argv.slice(saveIndex + 1) : argv;
|
|
1514
|
+
let formatIndex = -1;
|
|
1515
|
+
let formatConsumesNextToken = false;
|
|
1516
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
1517
|
+
const token = tokens[i];
|
|
1518
|
+
if (token === "--format") {
|
|
1519
|
+
formatIndex = i;
|
|
1520
|
+
formatConsumesNextToken = true;
|
|
1521
|
+
break;
|
|
1522
|
+
}
|
|
1523
|
+
if (token === `--format=${formatValue}`) {
|
|
1524
|
+
formatIndex = i;
|
|
1525
|
+
break;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (formatIndex === -1)
|
|
1529
|
+
return false;
|
|
1530
|
+
// If the name appears as a standalone token before --format, it's the
|
|
1531
|
+
// real positional and --format did not consume it.
|
|
1532
|
+
if (tokens.slice(0, formatIndex).includes(name))
|
|
1533
|
+
return false;
|
|
1534
|
+
// If --format has a space-separated value, skip past the value token
|
|
1535
|
+
// when scanning after the flag; otherwise start right after the flag.
|
|
1536
|
+
const firstTokenAfterFormat = formatIndex + (formatConsumesNextToken ? 2 : 1);
|
|
1537
|
+
if (tokens.slice(firstTokenAfterFormat).includes(name))
|
|
1538
|
+
return false;
|
|
1539
|
+
return true;
|
|
1540
|
+
}
|
|
1173
1541
|
const cloneCommand = defineCommand({
|
|
1174
1542
|
meta: {
|
|
1175
1543
|
name: "clone",
|
|
@@ -1356,6 +1724,364 @@ const feedbackCommand = defineCommand({
|
|
|
1356
1724
|
});
|
|
1357
1725
|
},
|
|
1358
1726
|
});
|
|
1727
|
+
function tryReadStdinText() {
|
|
1728
|
+
if (process.stdin.isTTY)
|
|
1729
|
+
return undefined;
|
|
1730
|
+
const input = fs.readFileSync(0, "utf8");
|
|
1731
|
+
return input.length > 0 ? input : undefined;
|
|
1732
|
+
}
|
|
1733
|
+
function normalizeMarkdownAssetName(name, fallback) {
|
|
1734
|
+
const trimmed = (name ?? fallback)
|
|
1735
|
+
.trim()
|
|
1736
|
+
.replace(/\\/g, "/")
|
|
1737
|
+
.replace(/^\/+|\/+$/g, "")
|
|
1738
|
+
.replace(/\.md$/i, "");
|
|
1739
|
+
if (!trimmed)
|
|
1740
|
+
throw new UsageError("Asset name cannot be empty.");
|
|
1741
|
+
const segments = trimmed.split("/");
|
|
1742
|
+
if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
|
|
1743
|
+
throw new UsageError("Asset name must be a relative path without '.' or '..' segments.");
|
|
1744
|
+
}
|
|
1745
|
+
return trimmed;
|
|
1746
|
+
}
|
|
1747
|
+
function slugifyAssetName(value, fallbackPrefix) {
|
|
1748
|
+
const slug = value
|
|
1749
|
+
.toLowerCase()
|
|
1750
|
+
.replace(/^[#>\-\s]+/, "")
|
|
1751
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1752
|
+
.replace(/^-+|-+$/g, "")
|
|
1753
|
+
.slice(0, MAX_CAPTURED_ASSET_SLUG_LENGTH);
|
|
1754
|
+
return slug || `${fallbackPrefix}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
1755
|
+
}
|
|
1756
|
+
function inferAssetName(content, fallbackPrefix, preferred) {
|
|
1757
|
+
const firstNonEmptyLine = content
|
|
1758
|
+
.split(/\r?\n/)
|
|
1759
|
+
.map((line) => line.trim())
|
|
1760
|
+
.find((line) => line.length > 0);
|
|
1761
|
+
const basis = preferred?.trim() || firstNonEmptyLine || fallbackPrefix;
|
|
1762
|
+
return slugifyAssetName(basis, fallbackPrefix);
|
|
1763
|
+
}
|
|
1764
|
+
function readMemoryContent(contentArg) {
|
|
1765
|
+
const content = contentArg ?? tryReadStdinText();
|
|
1766
|
+
if (!content?.trim()) {
|
|
1767
|
+
throw new UsageError("Memory content is required. Pass quoted text or pipe markdown into stdin.");
|
|
1768
|
+
}
|
|
1769
|
+
return content;
|
|
1770
|
+
}
|
|
1771
|
+
function readKnowledgeContent(source) {
|
|
1772
|
+
if (source === "-") {
|
|
1773
|
+
const content = tryReadStdinText();
|
|
1774
|
+
if (!content?.trim()) {
|
|
1775
|
+
throw new UsageError("No stdin content received. Pipe a document into stdin or pass a file path.");
|
|
1776
|
+
}
|
|
1777
|
+
return { content };
|
|
1778
|
+
}
|
|
1779
|
+
const resolvedSource = path.resolve(source);
|
|
1780
|
+
let stat;
|
|
1781
|
+
try {
|
|
1782
|
+
stat = fs.statSync(resolvedSource);
|
|
1783
|
+
}
|
|
1784
|
+
catch {
|
|
1785
|
+
throw new UsageError(`Knowledge source not found: "${source}". Pass a readable file path or "-" for stdin.`);
|
|
1786
|
+
}
|
|
1787
|
+
if (!stat.isFile()) {
|
|
1788
|
+
throw new UsageError(`Knowledge source must be a file: "${source}".`);
|
|
1789
|
+
}
|
|
1790
|
+
return {
|
|
1791
|
+
content: fs.readFileSync(resolvedSource, "utf8"),
|
|
1792
|
+
preferredName: path.basename(resolvedSource, path.extname(resolvedSource)),
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
function writeMarkdownAsset(options) {
|
|
1796
|
+
const stashDir = resolveStashDir();
|
|
1797
|
+
const typeRoot = path.join(stashDir, options.type === "knowledge" ? "knowledge" : "memories");
|
|
1798
|
+
fs.mkdirSync(typeRoot, { recursive: true });
|
|
1799
|
+
const normalizedName = normalizeMarkdownAssetName(options.name, inferAssetName(options.content, options.fallbackPrefix, options.preferredName));
|
|
1800
|
+
const assetPath = resolveAssetPathFromName(options.type, typeRoot, normalizedName);
|
|
1801
|
+
if (!isWithin(assetPath, typeRoot)) {
|
|
1802
|
+
throw new UsageError(`Resolved ${options.type} path escapes the stash: "${normalizedName}"`);
|
|
1803
|
+
}
|
|
1804
|
+
if (fs.existsSync(assetPath) && !options.force) {
|
|
1805
|
+
throw new UsageError(`${options.type === "knowledge" ? "Knowledge" : "Memory"} "${normalizedName}" already exists. Re-run with --force to overwrite it.`);
|
|
1806
|
+
}
|
|
1807
|
+
fs.mkdirSync(path.dirname(assetPath), { recursive: true });
|
|
1808
|
+
fs.writeFileSync(assetPath, options.content.endsWith("\n") ? options.content : `${options.content}\n`, "utf8");
|
|
1809
|
+
return {
|
|
1810
|
+
ref: `${options.type}:${normalizedName}`,
|
|
1811
|
+
path: assetPath,
|
|
1812
|
+
stashDir,
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
const workflowStartCommand = defineCommand({
|
|
1816
|
+
meta: {
|
|
1817
|
+
name: "start",
|
|
1818
|
+
description: "Start a new workflow run",
|
|
1819
|
+
},
|
|
1820
|
+
args: {
|
|
1821
|
+
ref: { type: "positional", description: "Workflow ref (workflow:<name>)", required: true },
|
|
1822
|
+
params: { type: "string", description: "Workflow parameters as a JSON object" },
|
|
1823
|
+
},
|
|
1824
|
+
async run({ args }) {
|
|
1825
|
+
await runWithJsonErrors(async () => {
|
|
1826
|
+
const result = await startWorkflowRun(args.ref, parseWorkflowJsonObject(args.params, "--params"));
|
|
1827
|
+
output("workflow-start", result);
|
|
1828
|
+
});
|
|
1829
|
+
},
|
|
1830
|
+
});
|
|
1831
|
+
const workflowNextCommand = defineCommand({
|
|
1832
|
+
meta: {
|
|
1833
|
+
name: "next",
|
|
1834
|
+
description: "Show the next actionable workflow step, auto-starting a run when passed a workflow ref",
|
|
1835
|
+
},
|
|
1836
|
+
args: {
|
|
1837
|
+
target: { type: "positional", description: "Workflow run id or workflow ref", required: true },
|
|
1838
|
+
params: { type: "string", description: "Workflow parameters as a JSON object (only for auto-started runs)" },
|
|
1839
|
+
},
|
|
1840
|
+
async run({ args }) {
|
|
1841
|
+
await runWithJsonErrors(async () => {
|
|
1842
|
+
const parsedParams = args.params ? parseWorkflowJsonObject(args.params, "--params") : undefined;
|
|
1843
|
+
const result = await getNextWorkflowStep(args.target, parsedParams);
|
|
1844
|
+
output("workflow-next", result);
|
|
1845
|
+
});
|
|
1846
|
+
},
|
|
1847
|
+
});
|
|
1848
|
+
const workflowCompleteCommand = defineCommand({
|
|
1849
|
+
meta: {
|
|
1850
|
+
name: "complete",
|
|
1851
|
+
description: "Update a workflow step state and persist notes/evidence",
|
|
1852
|
+
},
|
|
1853
|
+
args: {
|
|
1854
|
+
runId: { type: "positional", description: "Workflow run id", required: true },
|
|
1855
|
+
step: { type: "string", description: "Workflow step id", required: true },
|
|
1856
|
+
state: {
|
|
1857
|
+
type: "string",
|
|
1858
|
+
description: `Step state (default: completed). One of: ${WORKFLOW_STEP_STATES.join(", ")}.`,
|
|
1859
|
+
},
|
|
1860
|
+
notes: { type: "string", description: "Notes for the completed step" },
|
|
1861
|
+
evidence: { type: "string", description: "Evidence JSON object for the step" },
|
|
1862
|
+
},
|
|
1863
|
+
async run({ args }) {
|
|
1864
|
+
await runWithJsonErrors(async () => {
|
|
1865
|
+
const result = completeWorkflowStep({
|
|
1866
|
+
runId: args.runId,
|
|
1867
|
+
stepId: args.step,
|
|
1868
|
+
status: parseWorkflowStepState(args.state),
|
|
1869
|
+
notes: args.notes,
|
|
1870
|
+
evidence: args.evidence ? parseWorkflowJsonObject(args.evidence, "--evidence") : undefined,
|
|
1871
|
+
});
|
|
1872
|
+
output("workflow-complete", result);
|
|
1873
|
+
});
|
|
1874
|
+
},
|
|
1875
|
+
});
|
|
1876
|
+
const workflowStatusCommand = defineCommand({
|
|
1877
|
+
meta: {
|
|
1878
|
+
name: "status",
|
|
1879
|
+
description: "Show full workflow run state for review or resume",
|
|
1880
|
+
},
|
|
1881
|
+
args: {
|
|
1882
|
+
target: { type: "positional", description: "Workflow run id or workflow ref (workflow:<name>)", required: true },
|
|
1883
|
+
},
|
|
1884
|
+
run({ args }) {
|
|
1885
|
+
return runWithJsonErrors(() => {
|
|
1886
|
+
const target = args.target;
|
|
1887
|
+
// Check if target looks like a workflow ref
|
|
1888
|
+
const parsed = (() => {
|
|
1889
|
+
try {
|
|
1890
|
+
return parseAssetRef(target);
|
|
1891
|
+
}
|
|
1892
|
+
catch {
|
|
1893
|
+
return null;
|
|
1894
|
+
}
|
|
1895
|
+
})();
|
|
1896
|
+
if (parsed?.type === "workflow") {
|
|
1897
|
+
const ref = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`;
|
|
1898
|
+
const { runs } = listWorkflowRuns({ workflowRef: ref });
|
|
1899
|
+
if (runs.length === 0) {
|
|
1900
|
+
throw new NotFoundError(`No workflow runs found for ${ref}`);
|
|
1901
|
+
}
|
|
1902
|
+
const mostRecent = runs[0];
|
|
1903
|
+
if (!mostRecent)
|
|
1904
|
+
throw new NotFoundError(`No workflow runs found for ${ref}`);
|
|
1905
|
+
const result = getWorkflowStatus(mostRecent.id);
|
|
1906
|
+
output("workflow-status", result);
|
|
1907
|
+
}
|
|
1908
|
+
else {
|
|
1909
|
+
const result = getWorkflowStatus(target);
|
|
1910
|
+
output("workflow-status", result);
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
},
|
|
1914
|
+
});
|
|
1915
|
+
const workflowListCommand = defineCommand({
|
|
1916
|
+
meta: {
|
|
1917
|
+
name: "list",
|
|
1918
|
+
description: "List workflow runs",
|
|
1919
|
+
},
|
|
1920
|
+
args: {
|
|
1921
|
+
ref: { type: "string", description: "Filter to one workflow ref" },
|
|
1922
|
+
active: { type: "boolean", description: "Only show active runs", default: false },
|
|
1923
|
+
},
|
|
1924
|
+
run({ args }) {
|
|
1925
|
+
return runWithJsonErrors(() => {
|
|
1926
|
+
const result = listWorkflowRuns({ workflowRef: args.ref, activeOnly: args.active });
|
|
1927
|
+
output("workflow-list", result);
|
|
1928
|
+
});
|
|
1929
|
+
},
|
|
1930
|
+
});
|
|
1931
|
+
const workflowCreateCommand = defineCommand({
|
|
1932
|
+
meta: {
|
|
1933
|
+
name: "create",
|
|
1934
|
+
description: "Create a workflow markdown document in the working stash",
|
|
1935
|
+
},
|
|
1936
|
+
args: {
|
|
1937
|
+
name: { type: "positional", description: "Workflow name", required: true },
|
|
1938
|
+
from: { type: "string", description: "Import and validate markdown from an existing file" },
|
|
1939
|
+
force: {
|
|
1940
|
+
type: "boolean",
|
|
1941
|
+
description: "Overwrite an existing workflow (requires --from or --reset)",
|
|
1942
|
+
default: false,
|
|
1943
|
+
},
|
|
1944
|
+
reset: {
|
|
1945
|
+
type: "boolean",
|
|
1946
|
+
description: "Explicitly replace an existing workflow with a fresh template (use with --force)",
|
|
1947
|
+
default: false,
|
|
1948
|
+
},
|
|
1949
|
+
},
|
|
1950
|
+
run({ args }) {
|
|
1951
|
+
return runWithJsonErrors(() => {
|
|
1952
|
+
const namePattern = /^[a-z0-9][a-z0-9._/-]*$/;
|
|
1953
|
+
if (!namePattern.test(args.name)) {
|
|
1954
|
+
throw new UsageError("Workflow name must start with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, dots, underscores, and slashes.");
|
|
1955
|
+
}
|
|
1956
|
+
if (args.force && !args.from && !args.reset) {
|
|
1957
|
+
throw new UsageError("Refusing to overwrite with template: pass --from <file> to replace content, or --reset to explicitly replace with a fresh template.");
|
|
1958
|
+
}
|
|
1959
|
+
const result = createWorkflowAsset({
|
|
1960
|
+
name: args.name,
|
|
1961
|
+
from: args.from,
|
|
1962
|
+
force: args.force,
|
|
1963
|
+
});
|
|
1964
|
+
output("workflow-create", { ok: true, ...result });
|
|
1965
|
+
});
|
|
1966
|
+
},
|
|
1967
|
+
});
|
|
1968
|
+
const workflowTemplateCommand = defineCommand({
|
|
1969
|
+
meta: {
|
|
1970
|
+
name: "template",
|
|
1971
|
+
description: "Print a valid workflow markdown template",
|
|
1972
|
+
},
|
|
1973
|
+
run() {
|
|
1974
|
+
process.stdout.write(getWorkflowTemplate());
|
|
1975
|
+
},
|
|
1976
|
+
});
|
|
1977
|
+
const workflowResumeCommand = defineCommand({
|
|
1978
|
+
meta: {
|
|
1979
|
+
name: "resume",
|
|
1980
|
+
description: "Resume a blocked or failed workflow run, flipping it back to active",
|
|
1981
|
+
},
|
|
1982
|
+
args: {
|
|
1983
|
+
runId: { type: "positional", description: "Workflow run id", required: true },
|
|
1984
|
+
},
|
|
1985
|
+
run({ args }) {
|
|
1986
|
+
return runWithJsonErrors(() => {
|
|
1987
|
+
const result = resumeWorkflowRun(args.runId);
|
|
1988
|
+
output("workflow-run", result);
|
|
1989
|
+
});
|
|
1990
|
+
},
|
|
1991
|
+
});
|
|
1992
|
+
const workflowCommand = defineCommand({
|
|
1993
|
+
meta: {
|
|
1994
|
+
name: "workflow",
|
|
1995
|
+
description: "Author, inspect, and execute step-by-step workflow assets",
|
|
1996
|
+
},
|
|
1997
|
+
subCommands: {
|
|
1998
|
+
start: workflowStartCommand,
|
|
1999
|
+
next: workflowNextCommand,
|
|
2000
|
+
complete: workflowCompleteCommand,
|
|
2001
|
+
status: workflowStatusCommand,
|
|
2002
|
+
list: workflowListCommand,
|
|
2003
|
+
create: workflowCreateCommand,
|
|
2004
|
+
template: workflowTemplateCommand,
|
|
2005
|
+
resume: workflowResumeCommand,
|
|
2006
|
+
},
|
|
2007
|
+
run({ args }) {
|
|
2008
|
+
return runWithJsonErrors(() => {
|
|
2009
|
+
if (hasWorkflowSubcommand(args))
|
|
2010
|
+
return;
|
|
2011
|
+
output("workflow-list", listWorkflowRuns({ activeOnly: true }));
|
|
2012
|
+
});
|
|
2013
|
+
},
|
|
2014
|
+
});
|
|
2015
|
+
const rememberCommand = defineCommand({
|
|
2016
|
+
meta: {
|
|
2017
|
+
name: "remember",
|
|
2018
|
+
description: "Record a memory in the default stash",
|
|
2019
|
+
},
|
|
2020
|
+
args: {
|
|
2021
|
+
content: {
|
|
2022
|
+
type: "positional",
|
|
2023
|
+
description: "Memory content. Omit to read markdown from stdin.",
|
|
2024
|
+
required: false,
|
|
2025
|
+
},
|
|
2026
|
+
name: {
|
|
2027
|
+
type: "string",
|
|
2028
|
+
description: "Memory name (defaults to a slug from the content)",
|
|
2029
|
+
},
|
|
2030
|
+
force: {
|
|
2031
|
+
type: "boolean",
|
|
2032
|
+
description: "Overwrite an existing memory with the same name",
|
|
2033
|
+
default: false,
|
|
2034
|
+
},
|
|
2035
|
+
},
|
|
2036
|
+
run({ args }) {
|
|
2037
|
+
return runWithJsonErrors(() => {
|
|
2038
|
+
const result = writeMarkdownAsset({
|
|
2039
|
+
type: "memory",
|
|
2040
|
+
content: readMemoryContent(args.content),
|
|
2041
|
+
name: args.name,
|
|
2042
|
+
fallbackPrefix: "memory",
|
|
2043
|
+
force: args.force,
|
|
2044
|
+
});
|
|
2045
|
+
output("remember", { ok: true, ...result });
|
|
2046
|
+
});
|
|
2047
|
+
},
|
|
2048
|
+
});
|
|
2049
|
+
const importKnowledgeCommand = defineCommand({
|
|
2050
|
+
meta: {
|
|
2051
|
+
name: "import",
|
|
2052
|
+
description: "Import a knowledge document into the default stash",
|
|
2053
|
+
},
|
|
2054
|
+
args: {
|
|
2055
|
+
source: {
|
|
2056
|
+
type: "positional",
|
|
2057
|
+
description: 'Source file path, or "-" to read from stdin',
|
|
2058
|
+
required: true,
|
|
2059
|
+
},
|
|
2060
|
+
name: {
|
|
2061
|
+
type: "string",
|
|
2062
|
+
description: "Knowledge name (defaults to the source filename or content slug)",
|
|
2063
|
+
},
|
|
2064
|
+
force: {
|
|
2065
|
+
type: "boolean",
|
|
2066
|
+
description: "Overwrite an existing knowledge document with the same name",
|
|
2067
|
+
default: false,
|
|
2068
|
+
},
|
|
2069
|
+
},
|
|
2070
|
+
async run({ args }) {
|
|
2071
|
+
return runWithJsonErrors(async () => {
|
|
2072
|
+
const { content, preferredName } = readKnowledgeContent(args.source);
|
|
2073
|
+
const result = writeMarkdownAsset({
|
|
2074
|
+
type: "knowledge",
|
|
2075
|
+
content,
|
|
2076
|
+
name: args.name,
|
|
2077
|
+
fallbackPrefix: "knowledge",
|
|
2078
|
+
preferredName,
|
|
2079
|
+
force: args.force,
|
|
2080
|
+
});
|
|
2081
|
+
output("import", { ok: true, source: args.source, ...result });
|
|
2082
|
+
});
|
|
2083
|
+
},
|
|
2084
|
+
});
|
|
1359
2085
|
const hintsCommand = defineCommand({
|
|
1360
2086
|
meta: {
|
|
1361
2087
|
name: "hints",
|
|
@@ -1401,6 +2127,503 @@ const completionsCommand = defineCommand({
|
|
|
1401
2127
|
}
|
|
1402
2128
|
},
|
|
1403
2129
|
});
|
|
2130
|
+
function normalizeToggleTarget(target) {
|
|
2131
|
+
const normalized = target.trim().toLowerCase();
|
|
2132
|
+
if (normalized === "skills.sh" || normalized === "skills-sh")
|
|
2133
|
+
return "skills.sh";
|
|
2134
|
+
if (normalized === "context-hub")
|
|
2135
|
+
return "context-hub";
|
|
2136
|
+
throw new UsageError(`Unsupported target "${target}". Supported targets: skills.sh, context-hub`);
|
|
2137
|
+
}
|
|
2138
|
+
function toggleSkillsShRegistry(enabled) {
|
|
2139
|
+
const config = loadUserConfig();
|
|
2140
|
+
const registries = (config.registries ?? DEFAULT_CONFIG.registries ?? []).map((registry) => ({ ...registry }));
|
|
2141
|
+
const idx = registries.findIndex((registry) => registry.provider === SKILLS_SH_PROVIDER || registry.name === SKILLS_SH_NAME || registry.url === SKILLS_SH_URL);
|
|
2142
|
+
if (idx >= 0) {
|
|
2143
|
+
const existing = registries[idx];
|
|
2144
|
+
const wasEnabled = existing.enabled !== false;
|
|
2145
|
+
existing.enabled = enabled;
|
|
2146
|
+
saveConfig({ ...config, registries });
|
|
2147
|
+
return { changed: wasEnabled !== enabled, component: SKILLS_SH_NAME, enabled };
|
|
2148
|
+
}
|
|
2149
|
+
if (!enabled) {
|
|
2150
|
+
// Materialize the skills.sh registry explicitly if absent.
|
|
2151
|
+
registries.push({ url: SKILLS_SH_URL, name: SKILLS_SH_NAME, provider: SKILLS_SH_PROVIDER, enabled: false });
|
|
2152
|
+
saveConfig({ ...config, registries });
|
|
2153
|
+
return { changed: true, component: SKILLS_SH_NAME, enabled: false };
|
|
2154
|
+
}
|
|
2155
|
+
registries.push({ url: SKILLS_SH_URL, name: SKILLS_SH_NAME, provider: SKILLS_SH_PROVIDER, enabled: true });
|
|
2156
|
+
saveConfig({ ...config, registries });
|
|
2157
|
+
return { changed: true, component: SKILLS_SH_NAME, enabled: true };
|
|
2158
|
+
}
|
|
2159
|
+
function toggleContextHubStash(enabled) {
|
|
2160
|
+
const config = loadUserConfig();
|
|
2161
|
+
const stashes = [...(config.stashes ?? [])];
|
|
2162
|
+
const idx = stashes.findIndex((stash) => stash.name === CONTEXT_HUB_ALIAS_REF || stash.url === CONTEXT_HUB_ALIAS_URL);
|
|
2163
|
+
if (idx >= 0) {
|
|
2164
|
+
const existing = stashes[idx];
|
|
2165
|
+
const wasEnabled = existing.enabled !== false;
|
|
2166
|
+
existing.enabled = enabled;
|
|
2167
|
+
saveConfig({ ...config, stashes });
|
|
2168
|
+
return { changed: wasEnabled !== enabled, component: CONTEXT_HUB_ALIAS_REF, enabled };
|
|
2169
|
+
}
|
|
2170
|
+
if (!enabled) {
|
|
2171
|
+
return { changed: false, component: CONTEXT_HUB_ALIAS_REF, enabled: false };
|
|
2172
|
+
}
|
|
2173
|
+
stashes.push({ type: "git", url: CONTEXT_HUB_ALIAS_URL, name: CONTEXT_HUB_ALIAS_REF, enabled: true });
|
|
2174
|
+
saveConfig({ ...config, stashes });
|
|
2175
|
+
return { changed: true, component: CONTEXT_HUB_ALIAS_REF, enabled: true };
|
|
2176
|
+
}
|
|
2177
|
+
function toggleComponent(targetRaw, enabled) {
|
|
2178
|
+
const target = normalizeToggleTarget(targetRaw);
|
|
2179
|
+
if (target === "skills.sh")
|
|
2180
|
+
return toggleSkillsShRegistry(enabled);
|
|
2181
|
+
return toggleContextHubStash(enabled);
|
|
2182
|
+
}
|
|
2183
|
+
const enableCommand = defineCommand({
|
|
2184
|
+
meta: { name: "enable", description: "Enable an optional component (skills.sh or context-hub)" },
|
|
2185
|
+
args: {
|
|
2186
|
+
target: { type: "positional", description: "Component to enable (skills.sh|context-hub)", required: true },
|
|
2187
|
+
},
|
|
2188
|
+
run({ args }) {
|
|
2189
|
+
return runWithJsonErrors(() => {
|
|
2190
|
+
const result = toggleComponent(args.target, true);
|
|
2191
|
+
output("enable", result);
|
|
2192
|
+
});
|
|
2193
|
+
},
|
|
2194
|
+
});
|
|
2195
|
+
const disableCommand = defineCommand({
|
|
2196
|
+
meta: { name: "disable", description: "Disable an optional component (skills.sh or context-hub)" },
|
|
2197
|
+
args: {
|
|
2198
|
+
target: { type: "positional", description: "Component to disable (skills.sh|context-hub)", required: true },
|
|
2199
|
+
},
|
|
2200
|
+
run({ args }) {
|
|
2201
|
+
return runWithJsonErrors(() => {
|
|
2202
|
+
const result = toggleComponent(args.target, false);
|
|
2203
|
+
output("disable", result);
|
|
2204
|
+
});
|
|
2205
|
+
},
|
|
2206
|
+
});
|
|
2207
|
+
// ── vault ───────────────────────────────────────────────────────────────────
|
|
2208
|
+
//
|
|
2209
|
+
// `akm vault` manages secrets stored in `.env` files under the vaults/
|
|
2210
|
+
// asset directory. Values are NEVER written to stdout. `vault load` is
|
|
2211
|
+
// the only value-emitting path: it parses the vault with dotenv, writes
|
|
2212
|
+
// a safely-escaped shell script to a mode-0600 temp file, and emits only
|
|
2213
|
+
// `. <temp>; rm -f <temp>` on stdout for `eval`. The shell reads values
|
|
2214
|
+
// from the temp file — they never transit through akm's stdout.
|
|
2215
|
+
function resolveVaultPath(ref) {
|
|
2216
|
+
const stashDir = resolveStashDir({ readOnly: true });
|
|
2217
|
+
const parsed = parseAssetRef(ref.includes(":") ? ref : `vault:${ref}`);
|
|
2218
|
+
if (parsed.type !== "vault") {
|
|
2219
|
+
throw new UsageError(`Expected a vault ref (vault:<name>); got "${ref}".`);
|
|
2220
|
+
}
|
|
2221
|
+
const typeRoot = path.join(stashDir, "vaults");
|
|
2222
|
+
const absPath = resolveAssetPathFromName("vault", typeRoot, parsed.name);
|
|
2223
|
+
return { name: parsed.name, absPath };
|
|
2224
|
+
}
|
|
2225
|
+
/**
|
|
2226
|
+
* Walk `vaults/` recursively and return one entry per `.env` file, using the
|
|
2227
|
+
* vault asset spec's canonical-name logic so listing matches what the
|
|
2228
|
+
* matcher/asset-spec actually resolves (e.g. `vaults/team/prod.env` →
|
|
2229
|
+
* `vault:team/prod`, `vaults/team/.env` → `vault:team/default`).
|
|
2230
|
+
*/
|
|
2231
|
+
function listVaultsRecursive(listKeysFn) {
|
|
2232
|
+
const stashDir = resolveStashDir({ readOnly: true });
|
|
2233
|
+
const vaultsDir = path.join(stashDir, "vaults");
|
|
2234
|
+
const result = [];
|
|
2235
|
+
if (!fs.existsSync(vaultsDir))
|
|
2236
|
+
return result;
|
|
2237
|
+
const walk = (dir) => {
|
|
2238
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2239
|
+
const full = path.join(dir, entry.name);
|
|
2240
|
+
if (entry.isDirectory()) {
|
|
2241
|
+
walk(full);
|
|
2242
|
+
continue;
|
|
2243
|
+
}
|
|
2244
|
+
if (!entry.isFile())
|
|
2245
|
+
continue;
|
|
2246
|
+
if (entry.name !== ".env" && !entry.name.endsWith(".env"))
|
|
2247
|
+
continue;
|
|
2248
|
+
const canonical = deriveCanonicalAssetName("vault", vaultsDir, full);
|
|
2249
|
+
if (!canonical)
|
|
2250
|
+
continue;
|
|
2251
|
+
const { keys } = listKeysFn(full);
|
|
2252
|
+
result.push({ ref: `vault:${canonical}`, path: full, keyCount: keys.length });
|
|
2253
|
+
}
|
|
2254
|
+
};
|
|
2255
|
+
walk(vaultsDir);
|
|
2256
|
+
return result;
|
|
2257
|
+
}
|
|
2258
|
+
const vaultListCommand = defineCommand({
|
|
2259
|
+
meta: { name: "list", description: "List vaults, or list keys (no values) inside one vault" },
|
|
2260
|
+
args: {
|
|
2261
|
+
ref: { type: "positional", description: "Optional vault ref (e.g. vault:prod or just prod)", required: false },
|
|
2262
|
+
},
|
|
2263
|
+
run({ args }) {
|
|
2264
|
+
return runWithJsonErrors(async () => {
|
|
2265
|
+
const { listKeys } = await import("./vault.js");
|
|
2266
|
+
if (args.ref) {
|
|
2267
|
+
const { name, absPath } = resolveVaultPath(args.ref);
|
|
2268
|
+
if (!fs.existsSync(absPath)) {
|
|
2269
|
+
throw new NotFoundError(`Vault not found: vault:${name}`);
|
|
2270
|
+
}
|
|
2271
|
+
const { keys, comments } = listKeys(absPath);
|
|
2272
|
+
output("vault-list", { ref: `vault:${name}`, path: absPath, keys, comments });
|
|
2273
|
+
return;
|
|
2274
|
+
}
|
|
2275
|
+
const vaults = listVaultsRecursive(listKeys);
|
|
2276
|
+
output("vault-list", { vaults });
|
|
2277
|
+
});
|
|
2278
|
+
},
|
|
2279
|
+
});
|
|
2280
|
+
const vaultCreateCommand = defineCommand({
|
|
2281
|
+
meta: { name: "create", description: "Create an empty vault file (no-op if it already exists)" },
|
|
2282
|
+
args: {
|
|
2283
|
+
name: { type: "positional", description: "Vault name (e.g. prod) — file becomes <name>.env", required: true },
|
|
2284
|
+
},
|
|
2285
|
+
run({ args }) {
|
|
2286
|
+
return runWithJsonErrors(async () => {
|
|
2287
|
+
const { createVault } = await import("./vault.js");
|
|
2288
|
+
const { name, absPath } = resolveVaultPath(args.name);
|
|
2289
|
+
createVault(absPath);
|
|
2290
|
+
output("vault-create", { ref: `vault:${name}`, path: absPath });
|
|
2291
|
+
});
|
|
2292
|
+
},
|
|
2293
|
+
});
|
|
2294
|
+
const vaultSetCommand = defineCommand({
|
|
2295
|
+
meta: {
|
|
2296
|
+
name: "set",
|
|
2297
|
+
description: 'Set a key in a vault. Value is written to disk and never echoed back. Accepts KEY=VALUE combined form or separate KEY VALUE args. Optionally attach a comment with --comment "description".',
|
|
2298
|
+
},
|
|
2299
|
+
args: {
|
|
2300
|
+
ref: { type: "positional", description: "Vault ref (e.g. vault:prod or just prod)", required: true },
|
|
2301
|
+
key: { type: "positional", description: "Key name (e.g. DB_URL) or KEY=VALUE combined form", required: true },
|
|
2302
|
+
value: {
|
|
2303
|
+
type: "positional",
|
|
2304
|
+
description: "Value to store (omit when using KEY=VALUE combined form)",
|
|
2305
|
+
required: false,
|
|
2306
|
+
},
|
|
2307
|
+
comment: { type: "string", description: "Optional comment written above the key line", required: false },
|
|
2308
|
+
},
|
|
2309
|
+
run({ args }) {
|
|
2310
|
+
return runWithJsonErrors(async () => {
|
|
2311
|
+
const { setKey } = await import("./vault.js");
|
|
2312
|
+
const { name, absPath } = resolveVaultPath(args.ref);
|
|
2313
|
+
let realKey;
|
|
2314
|
+
let realValue;
|
|
2315
|
+
if ((args.value === undefined || args.value === "") && args.key.includes("=")) {
|
|
2316
|
+
const eqIdx = args.key.indexOf("=");
|
|
2317
|
+
realKey = args.key.slice(0, eqIdx);
|
|
2318
|
+
realValue = args.key.slice(eqIdx + 1);
|
|
2319
|
+
}
|
|
2320
|
+
else {
|
|
2321
|
+
realKey = args.key;
|
|
2322
|
+
realValue = args.value ?? "";
|
|
2323
|
+
}
|
|
2324
|
+
setKey(absPath, realKey, realValue, args.comment);
|
|
2325
|
+
output("vault-set", { ref: `vault:${name}`, key: realKey, path: absPath });
|
|
2326
|
+
});
|
|
2327
|
+
},
|
|
2328
|
+
});
|
|
2329
|
+
const vaultUnsetCommand = defineCommand({
|
|
2330
|
+
meta: { name: "unset", description: "Remove a key from a vault" },
|
|
2331
|
+
args: {
|
|
2332
|
+
ref: { type: "positional", description: "Vault ref", required: true },
|
|
2333
|
+
key: { type: "positional", description: "Key name to remove", required: true },
|
|
2334
|
+
},
|
|
2335
|
+
run({ args }) {
|
|
2336
|
+
return runWithJsonErrors(async () => {
|
|
2337
|
+
const { unsetKey } = await import("./vault.js");
|
|
2338
|
+
const { name, absPath } = resolveVaultPath(args.ref);
|
|
2339
|
+
if (!fs.existsSync(absPath)) {
|
|
2340
|
+
throw new NotFoundError(`Vault not found: vault:${name}`);
|
|
2341
|
+
}
|
|
2342
|
+
const removed = unsetKey(absPath, args.key);
|
|
2343
|
+
output("vault-unset", { ref: `vault:${name}`, key: args.key, removed, path: absPath });
|
|
2344
|
+
});
|
|
2345
|
+
},
|
|
2346
|
+
});
|
|
2347
|
+
const vaultLoadCommand = defineCommand({
|
|
2348
|
+
meta: {
|
|
2349
|
+
name: "load",
|
|
2350
|
+
description: 'Emit a shell snippet that loads vault values into the current shell. Use: eval "$(akm vault load vault:<name>)". Values are parsed by dotenv, written to a mode-0600 temp file with safe single-quote escaping, then sourced and removed. No values appear on akm\'s stdout, and no shell expansion happens on raw vault content.',
|
|
2351
|
+
},
|
|
2352
|
+
args: {
|
|
2353
|
+
ref: { type: "positional", description: "Vault ref", required: true },
|
|
2354
|
+
},
|
|
2355
|
+
async run({ args }) {
|
|
2356
|
+
return runWithJsonErrors(async () => {
|
|
2357
|
+
// This command deliberately bypasses output()/JSON shaping. Its stdout
|
|
2358
|
+
// is a shell snippet intended for `eval`, not structured output.
|
|
2359
|
+
const { name, absPath } = resolveVaultPath(args.ref);
|
|
2360
|
+
if (!fs.existsSync(absPath)) {
|
|
2361
|
+
throw new NotFoundError(`Vault not found: vault:${name}`);
|
|
2362
|
+
}
|
|
2363
|
+
const { buildShellExportScript } = await import("./vault.js");
|
|
2364
|
+
const crypto = await import("node:crypto");
|
|
2365
|
+
const os = await import("node:os");
|
|
2366
|
+
// Parse via dotenv (no expansion, no code execution) and build a
|
|
2367
|
+
// script of literal `export KEY='value'` lines with `'\''` escaping.
|
|
2368
|
+
// Sourcing this is safe even if the raw vault file contained shell
|
|
2369
|
+
// metacharacters like $, backticks, or $(...).
|
|
2370
|
+
const script = buildShellExportScript(absPath);
|
|
2371
|
+
// Write to a mode-0600 temp file the shell can source.
|
|
2372
|
+
const tmpPath = path.join(os.tmpdir(), `akm-vault-${crypto.randomBytes(12).toString("hex")}.sh`);
|
|
2373
|
+
fs.writeFileSync(tmpPath, script, { mode: 0o600, encoding: "utf8" });
|
|
2374
|
+
try {
|
|
2375
|
+
fs.chmodSync(tmpPath, 0o600);
|
|
2376
|
+
}
|
|
2377
|
+
catch {
|
|
2378
|
+
/* best-effort on platforms without chmod */
|
|
2379
|
+
}
|
|
2380
|
+
const quotedTmp = `'${tmpPath.replace(/'/g, "'\\''")}'`;
|
|
2381
|
+
// Emit: source the temp file, then remove it — values reach bash only
|
|
2382
|
+
// via the temp file (mode 0600), never via akm's stdout.
|
|
2383
|
+
process.stdout.write(`. ${quotedTmp}; rm -f ${quotedTmp}\n`);
|
|
2384
|
+
});
|
|
2385
|
+
},
|
|
2386
|
+
});
|
|
2387
|
+
const vaultShowCommand = defineCommand({
|
|
2388
|
+
meta: { name: "show", description: "Show keys (no values) inside a vault — alias for `vault list <ref>`" },
|
|
2389
|
+
args: {
|
|
2390
|
+
ref: { type: "positional", description: "Vault ref (e.g. vault:prod or just prod)", required: true },
|
|
2391
|
+
},
|
|
2392
|
+
run({ args }) {
|
|
2393
|
+
return runWithJsonErrors(async () => {
|
|
2394
|
+
const { listKeys } = await import("./vault.js");
|
|
2395
|
+
const { name, absPath } = resolveVaultPath(args.ref);
|
|
2396
|
+
if (!fs.existsSync(absPath)) {
|
|
2397
|
+
throw new NotFoundError(`Vault not found: vault:${name}`);
|
|
2398
|
+
}
|
|
2399
|
+
const { keys, comments } = listKeys(absPath);
|
|
2400
|
+
output("vault-list", { ref: `vault:${name}`, path: absPath, keys, comments });
|
|
2401
|
+
});
|
|
2402
|
+
},
|
|
2403
|
+
});
|
|
2404
|
+
const vaultCommand = defineCommand({
|
|
2405
|
+
meta: {
|
|
2406
|
+
name: "vault",
|
|
2407
|
+
description: "Manage secret vaults (.env files). Lists keys + comments only — values never returned in structured output.",
|
|
2408
|
+
},
|
|
2409
|
+
subCommands: {
|
|
2410
|
+
list: vaultListCommand,
|
|
2411
|
+
show: vaultShowCommand,
|
|
2412
|
+
create: vaultCreateCommand,
|
|
2413
|
+
set: vaultSetCommand,
|
|
2414
|
+
unset: vaultUnsetCommand,
|
|
2415
|
+
load: vaultLoadCommand,
|
|
2416
|
+
},
|
|
2417
|
+
run({ args }) {
|
|
2418
|
+
return runWithJsonErrors(async () => {
|
|
2419
|
+
if (hasVaultSubcommand(args))
|
|
2420
|
+
return;
|
|
2421
|
+
// Default action: list all vaults
|
|
2422
|
+
const { listKeys } = await import("./vault.js");
|
|
2423
|
+
output("vault-list", { vaults: listVaultsRecursive(listKeys) });
|
|
2424
|
+
});
|
|
2425
|
+
},
|
|
2426
|
+
});
|
|
2427
|
+
// ── Wiki subcommands ─────────────────────────────────────────────────────────
|
|
2428
|
+
const wikiCreateCommand = defineCommand({
|
|
2429
|
+
meta: { name: "create", description: "Scaffold a new wiki under <stashDir>/wikis/<name>/" },
|
|
2430
|
+
args: {
|
|
2431
|
+
name: { type: "positional", description: "Wiki name (lowercase, digits, hyphens)", required: true },
|
|
2432
|
+
},
|
|
2433
|
+
run({ args }) {
|
|
2434
|
+
return runWithJsonErrors(async () => {
|
|
2435
|
+
const { createWiki } = await import("./wiki.js");
|
|
2436
|
+
const stashDir = resolveStashDir();
|
|
2437
|
+
const result = createWiki(stashDir, args.name);
|
|
2438
|
+
output("wiki-create", result);
|
|
2439
|
+
});
|
|
2440
|
+
},
|
|
2441
|
+
});
|
|
2442
|
+
const wikiListCommand = defineCommand({
|
|
2443
|
+
meta: { name: "list", description: "List wikis with page/raw counts and last-modified timestamps" },
|
|
2444
|
+
run() {
|
|
2445
|
+
return runWithJsonErrors(async () => {
|
|
2446
|
+
const { listWikis } = await import("./wiki.js");
|
|
2447
|
+
const stashDir = resolveStashDir();
|
|
2448
|
+
const wikis = listWikis(stashDir);
|
|
2449
|
+
output("wiki-list", { wikis });
|
|
2450
|
+
});
|
|
2451
|
+
},
|
|
2452
|
+
});
|
|
2453
|
+
const wikiShowCommand = defineCommand({
|
|
2454
|
+
meta: { name: "show", description: "Show a wiki's path, description, counts, and last 3 log entries" },
|
|
2455
|
+
args: {
|
|
2456
|
+
name: { type: "positional", description: "Wiki name", required: true },
|
|
2457
|
+
},
|
|
2458
|
+
run({ args }) {
|
|
2459
|
+
return runWithJsonErrors(async () => {
|
|
2460
|
+
const { showWiki } = await import("./wiki.js");
|
|
2461
|
+
const stashDir = resolveStashDir();
|
|
2462
|
+
const result = showWiki(stashDir, args.name);
|
|
2463
|
+
output("wiki-show", result);
|
|
2464
|
+
});
|
|
2465
|
+
},
|
|
2466
|
+
});
|
|
2467
|
+
const wikiRemoveCommand = defineCommand({
|
|
2468
|
+
meta: {
|
|
2469
|
+
name: "remove",
|
|
2470
|
+
description: "Remove a wiki. Preserves raw/ by default; pass --with-sources to also delete raw/",
|
|
2471
|
+
},
|
|
2472
|
+
args: {
|
|
2473
|
+
name: { type: "positional", description: "Wiki name", required: true },
|
|
2474
|
+
force: {
|
|
2475
|
+
type: "boolean",
|
|
2476
|
+
description: "Remove without prompting (required in non-interactive shells)",
|
|
2477
|
+
default: false,
|
|
2478
|
+
},
|
|
2479
|
+
"with-sources": {
|
|
2480
|
+
type: "boolean",
|
|
2481
|
+
description: "Also delete the raw/ directory (immutable ingested sources)",
|
|
2482
|
+
default: false,
|
|
2483
|
+
},
|
|
2484
|
+
},
|
|
2485
|
+
run({ args }) {
|
|
2486
|
+
return runWithJsonErrors(async () => {
|
|
2487
|
+
if (!args.force) {
|
|
2488
|
+
throw new UsageError("Refusing to remove without --force. Pass `--force` to confirm.");
|
|
2489
|
+
}
|
|
2490
|
+
const withSources = Boolean(args["with-sources"]);
|
|
2491
|
+
const { removeWiki } = await import("./wiki.js");
|
|
2492
|
+
const stashDir = resolveStashDir();
|
|
2493
|
+
const result = removeWiki(stashDir, args.name, { withSources });
|
|
2494
|
+
output("wiki-remove", result);
|
|
2495
|
+
});
|
|
2496
|
+
},
|
|
2497
|
+
});
|
|
2498
|
+
const wikiPagesCommand = defineCommand({
|
|
2499
|
+
meta: {
|
|
2500
|
+
name: "pages",
|
|
2501
|
+
description: "List wiki pages (ref + frontmatter description), excluding schema/index/log/raw",
|
|
2502
|
+
},
|
|
2503
|
+
args: {
|
|
2504
|
+
name: { type: "positional", description: "Wiki name", required: true },
|
|
2505
|
+
},
|
|
2506
|
+
run({ args }) {
|
|
2507
|
+
return runWithJsonErrors(async () => {
|
|
2508
|
+
const { listPages } = await import("./wiki.js");
|
|
2509
|
+
const stashDir = resolveStashDir();
|
|
2510
|
+
const pages = listPages(stashDir, args.name);
|
|
2511
|
+
output("wiki-pages", { wiki: args.name, pages });
|
|
2512
|
+
});
|
|
2513
|
+
},
|
|
2514
|
+
});
|
|
2515
|
+
const wikiSearchCommand = defineCommand({
|
|
2516
|
+
meta: { name: "search", description: "Search within a single wiki (scoped wrapper over `akm search --type wiki`)" },
|
|
2517
|
+
args: {
|
|
2518
|
+
name: { type: "positional", description: "Wiki name", required: true },
|
|
2519
|
+
query: { type: "positional", description: "Search query", required: true },
|
|
2520
|
+
limit: { type: "string", description: "Max hits (default 20)", required: false },
|
|
2521
|
+
},
|
|
2522
|
+
run({ args }) {
|
|
2523
|
+
return runWithJsonErrors(async () => {
|
|
2524
|
+
const { searchInWiki } = await import("./wiki.js");
|
|
2525
|
+
const stashDir = resolveStashDir();
|
|
2526
|
+
const wikiDir = path.join(stashDir, "wikis", args.name);
|
|
2527
|
+
if (!fs.existsSync(wikiDir)) {
|
|
2528
|
+
throw new NotFoundError(`Wiki not found: ${args.name}`);
|
|
2529
|
+
}
|
|
2530
|
+
const parsedLimit = args.limit ? Number(args.limit) : undefined;
|
|
2531
|
+
const limit = typeof parsedLimit === "number" && Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : undefined;
|
|
2532
|
+
const response = await searchInWiki({ stashDir, wikiName: args.name, query: args.query, limit });
|
|
2533
|
+
output("search", response);
|
|
2534
|
+
});
|
|
2535
|
+
},
|
|
2536
|
+
});
|
|
2537
|
+
const wikiStashCommand = defineCommand({
|
|
2538
|
+
meta: {
|
|
2539
|
+
name: "stash",
|
|
2540
|
+
description: "Copy a source into wikis/<name>/raw/<slug>.md with frontmatter. Source may be a file path or '-' for stdin.",
|
|
2541
|
+
},
|
|
2542
|
+
args: {
|
|
2543
|
+
name: { type: "positional", description: "Wiki name", required: true },
|
|
2544
|
+
source: { type: "positional", description: "Source file path, or '-' to read from stdin", required: true },
|
|
2545
|
+
as: { type: "string", description: "Preferred slug base (defaults to source filename or first-line slug)" },
|
|
2546
|
+
},
|
|
2547
|
+
run({ args }) {
|
|
2548
|
+
return runWithJsonErrors(async () => {
|
|
2549
|
+
const { stashRaw } = await import("./wiki.js");
|
|
2550
|
+
const { content, preferredName } = readKnowledgeContent(args.source);
|
|
2551
|
+
const stashDir = resolveStashDir();
|
|
2552
|
+
const result = stashRaw({
|
|
2553
|
+
stashDir,
|
|
2554
|
+
wikiName: args.name,
|
|
2555
|
+
content,
|
|
2556
|
+
preferredName: args.as ?? preferredName,
|
|
2557
|
+
explicitSlug: args.as !== undefined,
|
|
2558
|
+
});
|
|
2559
|
+
output("wiki-stash", { ok: true, wiki: args.name, source: args.source, ...result });
|
|
2560
|
+
});
|
|
2561
|
+
},
|
|
2562
|
+
});
|
|
2563
|
+
const wikiLintCommand = defineCommand({
|
|
2564
|
+
meta: {
|
|
2565
|
+
name: "lint",
|
|
2566
|
+
description: "Structural lint for a wiki: orphans, broken xrefs, missing descriptions, uncited raws, stale index",
|
|
2567
|
+
},
|
|
2568
|
+
args: {
|
|
2569
|
+
name: { type: "positional", description: "Wiki name", required: true },
|
|
2570
|
+
},
|
|
2571
|
+
async run({ args }) {
|
|
2572
|
+
let findingCount = 0;
|
|
2573
|
+
await runWithJsonErrors(async () => {
|
|
2574
|
+
const { lintWiki } = await import("./wiki.js");
|
|
2575
|
+
const stashDir = resolveStashDir();
|
|
2576
|
+
const report = lintWiki(stashDir, args.name);
|
|
2577
|
+
output("wiki-lint", report);
|
|
2578
|
+
findingCount = report.findings.length;
|
|
2579
|
+
});
|
|
2580
|
+
if (findingCount > 0)
|
|
2581
|
+
process.exit(1); // EXIT_GENERAL
|
|
2582
|
+
},
|
|
2583
|
+
});
|
|
2584
|
+
const wikiIngestCommand = defineCommand({
|
|
2585
|
+
meta: {
|
|
2586
|
+
name: "ingest",
|
|
2587
|
+
description: "Print the ingest workflow for this wiki. Does not perform the ingest; instructs the agent to.",
|
|
2588
|
+
},
|
|
2589
|
+
args: {
|
|
2590
|
+
name: { type: "positional", description: "Wiki name", required: true },
|
|
2591
|
+
},
|
|
2592
|
+
run({ args }) {
|
|
2593
|
+
return runWithJsonErrors(async () => {
|
|
2594
|
+
const { buildIngestWorkflow } = await import("./wiki.js");
|
|
2595
|
+
const stashDir = resolveStashDir();
|
|
2596
|
+
const result = buildIngestWorkflow(stashDir, args.name);
|
|
2597
|
+
output("wiki-ingest", result);
|
|
2598
|
+
});
|
|
2599
|
+
},
|
|
2600
|
+
});
|
|
2601
|
+
const wikiCommand = defineCommand({
|
|
2602
|
+
meta: {
|
|
2603
|
+
name: "wiki",
|
|
2604
|
+
description: "Manage multiple markdown wikis (Karpathy-style). akm surfaces (lifecycle, raw/, lint, index); the agent writes pages.",
|
|
2605
|
+
},
|
|
2606
|
+
subCommands: {
|
|
2607
|
+
create: wikiCreateCommand,
|
|
2608
|
+
list: wikiListCommand,
|
|
2609
|
+
show: wikiShowCommand,
|
|
2610
|
+
remove: wikiRemoveCommand,
|
|
2611
|
+
pages: wikiPagesCommand,
|
|
2612
|
+
search: wikiSearchCommand,
|
|
2613
|
+
stash: wikiStashCommand,
|
|
2614
|
+
lint: wikiLintCommand,
|
|
2615
|
+
ingest: wikiIngestCommand,
|
|
2616
|
+
},
|
|
2617
|
+
run({ args }) {
|
|
2618
|
+
return runWithJsonErrors(async () => {
|
|
2619
|
+
if (hasWikiSubcommand(args))
|
|
2620
|
+
return;
|
|
2621
|
+
// Default action: list wikis
|
|
2622
|
+
const { listWikis } = await import("./wiki.js");
|
|
2623
|
+
output("wiki-list", { wikis: listWikis(resolveStashDir()) });
|
|
2624
|
+
});
|
|
2625
|
+
},
|
|
2626
|
+
});
|
|
1404
2627
|
const main = defineCommand({
|
|
1405
2628
|
meta: {
|
|
1406
2629
|
name: "akm",
|
|
@@ -1408,8 +2631,8 @@ const main = defineCommand({
|
|
|
1408
2631
|
description: "Agent Kit Manager — search, show, and manage assets from your stash.",
|
|
1409
2632
|
},
|
|
1410
2633
|
args: {
|
|
1411
|
-
format: { type: "string", description: "Output format (json|text|yaml)" },
|
|
1412
|
-
detail: { type: "string", description: "Detail level (brief|normal|full)" },
|
|
2634
|
+
format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
|
|
2635
|
+
detail: { type: "string", description: "Detail level (brief|normal|full|summary)" },
|
|
1413
2636
|
quiet: { type: "boolean", alias: "q", description: "Suppress stderr warnings", default: false },
|
|
1414
2637
|
},
|
|
1415
2638
|
subCommands: {
|
|
@@ -1425,15 +2648,25 @@ const main = defineCommand({
|
|
|
1425
2648
|
search: searchCommand,
|
|
1426
2649
|
curate: curateCommand,
|
|
1427
2650
|
show: showCommand,
|
|
2651
|
+
workflow: workflowCommand,
|
|
2652
|
+
remember: rememberCommand,
|
|
2653
|
+
import: importKnowledgeCommand,
|
|
2654
|
+
save: saveCommand,
|
|
1428
2655
|
clone: cloneCommand,
|
|
1429
2656
|
registry: registryCommand,
|
|
1430
2657
|
config: configCommand,
|
|
2658
|
+
enable: enableCommand,
|
|
2659
|
+
disable: disableCommand,
|
|
1431
2660
|
feedback: feedbackCommand,
|
|
1432
2661
|
hints: hintsCommand,
|
|
1433
2662
|
completions: completionsCommand,
|
|
2663
|
+
vault: vaultCommand,
|
|
2664
|
+
wiki: wikiCommand,
|
|
1434
2665
|
},
|
|
1435
2666
|
});
|
|
1436
2667
|
const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "get", "set", "unset"]);
|
|
2668
|
+
const VAULT_SUBCOMMAND_SET = new Set(["list", "show", "create", "set", "unset", "load"]);
|
|
2669
|
+
const WIKI_SUBCOMMAND_SET = new Set(["create", "list", "show", "remove", "pages", "search", "stash", "lint", "ingest"]);
|
|
1437
2670
|
const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
|
|
1438
2671
|
// citty reads process.argv directly and does not accept a custom argv array,
|
|
1439
2672
|
// so we must replace process.argv with the normalized version before runMain.
|
|
@@ -1494,6 +2727,14 @@ function hasConfigSubcommand(args) {
|
|
|
1494
2727
|
const command = Array.isArray(args._) ? args._[0] : undefined;
|
|
1495
2728
|
return typeof command === "string" && CONFIG_SUBCOMMAND_SET.has(command);
|
|
1496
2729
|
}
|
|
2730
|
+
function hasVaultSubcommand(args) {
|
|
2731
|
+
const command = Array.isArray(args._) ? args._[0] : undefined;
|
|
2732
|
+
return typeof command === "string" && VAULT_SUBCOMMAND_SET.has(command);
|
|
2733
|
+
}
|
|
2734
|
+
function hasWikiSubcommand(args) {
|
|
2735
|
+
const command = Array.isArray(args._) ? args._[0] : undefined;
|
|
2736
|
+
return typeof command === "string" && WIKI_SUBCOMMAND_SET.has(command);
|
|
2737
|
+
}
|
|
1497
2738
|
/**
|
|
1498
2739
|
* Normalize argv so positional view-mode arguments after the asset ref
|
|
1499
2740
|
* are rewritten into internal flags that citty can parse.
|
|
@@ -1569,7 +2810,7 @@ function loadHints(detail = "normal") {
|
|
|
1569
2810
|
const fallback = detail === "full" ? EMBEDDED_HINTS_FULL : EMBEDDED_HINTS;
|
|
1570
2811
|
// Try reading from the docs/ directory (works in dev and when installed via npm)
|
|
1571
2812
|
try {
|
|
1572
|
-
const docsPath = path.resolve(import.meta.dir ?? __dirname, `../docs/${filename}`);
|
|
2813
|
+
const docsPath = path.resolve(import.meta.dir ?? __dirname, `../docs/agents/${filename}`);
|
|
1573
2814
|
if (fs.existsSync(docsPath)) {
|
|
1574
2815
|
return fs.readFileSync(docsPath, "utf8");
|
|
1575
2816
|
}
|
|
@@ -1582,18 +2823,25 @@ function loadHints(detail = "normal") {
|
|
|
1582
2823
|
}
|
|
1583
2824
|
const EMBEDDED_HINTS = `# akm CLI
|
|
1584
2825
|
|
|
1585
|
-
You have access to a searchable library of scripts, skills, commands, agents,
|
|
2826
|
+
You have access to a searchable library of scripts, skills, commands, agents, knowledge documents, workflows, wikis, and memories via \`akm\`. Search your sources first before writing something from scratch.
|
|
1586
2827
|
|
|
1587
2828
|
## Quick Reference
|
|
1588
2829
|
|
|
1589
2830
|
\`\`\`sh
|
|
1590
2831
|
akm search "<query>" # Search all sources
|
|
1591
2832
|
akm curate "<task>" # Curate the best matches for a task
|
|
1592
|
-
akm search "<query>" --type
|
|
2833
|
+
akm search "<query>" --type workflow # Filter to workflow assets
|
|
1593
2834
|
akm search "<query>" --source both # Also search registries
|
|
1594
2835
|
akm show <ref> # View asset details
|
|
2836
|
+
akm workflow next <ref> # Start or resume a workflow
|
|
2837
|
+
akm remember "Deployment needs VPN access" # Record a memory in your stash
|
|
2838
|
+
akm import ./notes/release-checklist.md # Import a knowledge doc into your stash
|
|
2839
|
+
akm wiki list # List available wikis
|
|
2840
|
+
akm wiki ingest <name> # Print the ingest workflow for a wiki
|
|
2841
|
+
akm feedback <ref> --positive|--negative # Record whether an asset helped
|
|
1595
2842
|
akm add <ref> # Add a source (npm, GitHub, git, local dir)
|
|
1596
2843
|
akm clone <ref> # Copy an asset to the working stash (optional --dest arg to clone to specific location)
|
|
2844
|
+
akm save # Commit (and push if writable remote) changes in the primary stash
|
|
1597
2845
|
akm registry search "<query>" # Search all registries
|
|
1598
2846
|
\`\`\`
|
|
1599
2847
|
|
|
@@ -1606,19 +2854,26 @@ akm registry search "<query>" # Search all registries
|
|
|
1606
2854
|
| command | A prompt template with placeholders to fill in |
|
|
1607
2855
|
| agent | A system prompt with model and tool hints |
|
|
1608
2856
|
| knowledge | A reference doc (use \`toc\` or \`section "..."\` to navigate) |
|
|
2857
|
+
| workflow | Parsed steps plus workflow-specific execution commands |
|
|
2858
|
+
| memory | Recalled context (read the content for background information) |
|
|
2859
|
+
| vault | Key names only; use vault commands to inspect or load values safely |
|
|
2860
|
+
| wiki | A page in a multi-wiki knowledge base. For any wiki task, start with \`akm wiki list\`, then \`akm wiki ingest <name>\` for the workflow. Run \`akm wiki -h\` for the full surface. |
|
|
2861
|
+
|
|
2862
|
+
When an asset meaningfully helps or fails, record that with \`akm feedback\` so
|
|
2863
|
+
future search ranking can learn from real usage.
|
|
1609
2864
|
|
|
1610
2865
|
Run \`akm -h\` for the full command reference.
|
|
1611
2866
|
`;
|
|
1612
2867
|
const EMBEDDED_HINTS_FULL = `# akm CLI — Full Reference
|
|
1613
2868
|
|
|
1614
|
-
You have access to a searchable library of scripts, skills, commands, agents,
|
|
2869
|
+
You have access to a searchable library of scripts, skills, commands, agents, knowledge documents, workflows, wikis, and memories via \`akm\`. Search your sources first before writing something from scratch.
|
|
1615
2870
|
|
|
1616
2871
|
## Search
|
|
1617
2872
|
|
|
1618
2873
|
\`\`\`sh
|
|
1619
2874
|
akm search "<query>" # Search all sources
|
|
1620
2875
|
akm curate "<task>" # Curate the best matches for a task
|
|
1621
|
-
akm search "<query>" --type
|
|
2876
|
+
akm search "<query>" --type workflow # Filter by asset type
|
|
1622
2877
|
akm search "<query>" --source both # Also search registries
|
|
1623
2878
|
akm search "<query>" --source registry # Search registries only
|
|
1624
2879
|
akm search "<query>" --limit 10 # Limit results
|
|
@@ -1627,7 +2882,7 @@ akm search "<query>" --detail full # Include scores, paths, timing
|
|
|
1627
2882
|
|
|
1628
2883
|
| Flag | Values | Default |
|
|
1629
2884
|
| --- | --- | --- |
|
|
1630
|
-
| \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`script\`, \`memory\`, \`any\` | \`any\` |
|
|
2885
|
+
| \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`workflow\`, \`script\`, \`memory\`, \`vault\`, \`wiki\`, \`any\` | \`any\` |
|
|
1631
2886
|
| \`--source\` | \`stash\`, \`registry\`, \`both\` | \`stash\` |
|
|
1632
2887
|
| \`--limit\` | number | \`20\` |
|
|
1633
2888
|
| \`--format\` | \`json\`, \`jsonl\`, \`text\`, \`yaml\` | \`json\` |
|
|
@@ -1641,7 +2896,7 @@ Combine search + follow-up hints into a dense summary for a task or prompt.
|
|
|
1641
2896
|
\`\`\`sh
|
|
1642
2897
|
akm curate "plan a release" # Pick top matches across asset types
|
|
1643
2898
|
akm curate "deploy a Bun app" --limit 3 # Keep the summary shorter
|
|
1644
|
-
akm curate "review architecture" --type
|
|
2899
|
+
akm curate "review architecture" --type workflow # Restrict to one asset type
|
|
1645
2900
|
\`\`\`
|
|
1646
2901
|
|
|
1647
2902
|
## Show
|
|
@@ -1653,6 +2908,7 @@ akm show script:deploy.sh # Show script (returns run command
|
|
|
1653
2908
|
akm show skill:code-review # Show skill (returns full content)
|
|
1654
2909
|
akm show command:release # Show command (returns template)
|
|
1655
2910
|
akm show agent:architect # Show agent (returns system prompt)
|
|
2911
|
+
akm show workflow:ship-release # Show parsed workflow steps
|
|
1656
2912
|
akm show knowledge:guide toc # Table of contents
|
|
1657
2913
|
akm show knowledge:guide section "Auth" # Specific section
|
|
1658
2914
|
akm show knowledge:guide lines 10 30 # Line range
|
|
@@ -1666,20 +2922,81 @@ akm show knowledge:my-doc # Show content (local or remote)
|
|
|
1666
2922
|
| command | \`template\`, \`description\`, \`parameters\` |
|
|
1667
2923
|
| agent | \`prompt\`, \`description\`, \`modelHint\`, \`toolPolicy\` |
|
|
1668
2924
|
| knowledge | \`content\` (with view modes: \`full\`, \`toc\`, \`frontmatter\`, \`section\`, \`lines\`) |
|
|
2925
|
+
| workflow | \`workflowTitle\`, \`workflowParameters\`, \`steps\` |
|
|
1669
2926
|
| memory | \`content\` (recalled context) |
|
|
2927
|
+
| vault | \`keys\`, \`comments\` |
|
|
2928
|
+
| wiki | \`content\` (same view modes as knowledge). For any wiki task, run \`akm wiki list\` then \`akm wiki ingest <name>\` for the workflow. |
|
|
1670
2929
|
|
|
1671
|
-
##
|
|
2930
|
+
## Capture Knowledge While You Work
|
|
1672
2931
|
|
|
1673
2932
|
\`\`\`sh
|
|
1674
|
-
akm
|
|
1675
|
-
akm
|
|
1676
|
-
akm
|
|
1677
|
-
akm
|
|
1678
|
-
akm
|
|
1679
|
-
akm
|
|
1680
|
-
akm
|
|
1681
|
-
akm
|
|
1682
|
-
|
|
2933
|
+
akm remember "Deployment needs VPN access" # Record a memory in your stash
|
|
2934
|
+
akm remember --name release-retro < notes.md # Save multiline memory from stdin
|
|
2935
|
+
akm import ./docs/auth-flow.md # Import a file as knowledge
|
|
2936
|
+
akm import - --name scratch-notes < notes.md # Import stdin as a knowledge doc
|
|
2937
|
+
akm workflow create ship-release # Create a workflow asset in the stash
|
|
2938
|
+
akm workflow next workflow:ship-release # Start or resume the next workflow step
|
|
2939
|
+
akm feedback skill:code-review --positive # Record that an asset helped
|
|
2940
|
+
akm feedback agent:reviewer --negative # Record that an asset missed the mark
|
|
2941
|
+
\`\`\`
|
|
2942
|
+
|
|
2943
|
+
Use \`akm feedback\` whenever an asset materially helps or fails so future search
|
|
2944
|
+
ranking can learn from actual usage.
|
|
2945
|
+
|
|
2946
|
+
## Wikis
|
|
2947
|
+
|
|
2948
|
+
Multi-wiki knowledge bases (Karpathy-style). Each wiki is a directory at
|
|
2949
|
+
\`<stashDir>/wikis/<name>/\` with \`schema.md\`, \`index.md\`, \`log.md\`, \`raw/\`,
|
|
2950
|
+
and agent-authored pages. akm owns lifecycle + raw-slug + lint + index
|
|
2951
|
+
regeneration; page edits use your native Read/Write/Edit tools.
|
|
2952
|
+
|
|
2953
|
+
\`\`\`sh
|
|
2954
|
+
akm wiki list # List wikis (name, pages, raws, last-modified)
|
|
2955
|
+
akm wiki create research # Scaffold a new wiki
|
|
2956
|
+
akm wiki show research # Path, description, counts, last 3 log entries
|
|
2957
|
+
akm wiki pages research # Page refs + descriptions (excludes schema/index/log/raw)
|
|
2958
|
+
akm wiki search research "attention" # Scoped search (equivalent to --type wiki --wiki research)
|
|
2959
|
+
akm wiki stash research ./paper.md # Copy source into raw/<slug>.md (never overwrites)
|
|
2960
|
+
echo "..." | akm wiki stash research - # stdin form
|
|
2961
|
+
akm wiki lint research # Structural checks: orphans, broken xrefs, uncited raws, stale index
|
|
2962
|
+
akm wiki ingest research # Print the ingest workflow for this wiki (no action)
|
|
2963
|
+
akm wiki remove research --force # Delete pages/schema/index/log; preserves raw/
|
|
2964
|
+
akm wiki remove research --force --with-sources # Full nuke, including raw/
|
|
2965
|
+
\`\`\`
|
|
2966
|
+
|
|
2967
|
+
**For any wiki task, start with \`akm wiki list\`, then \`akm wiki ingest <name>\`
|
|
2968
|
+
to get the step-by-step workflow.** Wiki pages are also addressable as
|
|
2969
|
+
\`wiki:<name>/<page-path>\` and show up in stash-wide \`akm search\` as
|
|
2970
|
+
\`type: wiki\`. No \`--llm\` anywhere — akm never reasons about page content.
|
|
2971
|
+
|
|
2972
|
+
## Vaults
|
|
2973
|
+
|
|
2974
|
+
Encrypted-at-rest key/value stores for secrets. Each vault is a \`.env\`-format
|
|
2975
|
+
file at \`<stashDir>/vaults/<name>.env\`.
|
|
2976
|
+
|
|
2977
|
+
\`\`\`sh
|
|
2978
|
+
akm vault create prod # Create a new vault
|
|
2979
|
+
akm vault set prod DB_URL postgres://... # Set a key (or KEY=VALUE combined form)
|
|
2980
|
+
akm vault set prod DB_URL=postgres://... # Combined KEY=VALUE form also works
|
|
2981
|
+
akm vault unset prod DB_URL # Remove a key
|
|
2982
|
+
akm vault list vault:prod # List key names (no values)
|
|
2983
|
+
akm vault show vault:prod # Same as list (alias)
|
|
2984
|
+
akm vault load vault:prod # Print export statements to source
|
|
2985
|
+
\`\`\`
|
|
2986
|
+
|
|
2987
|
+
## Workflows
|
|
2988
|
+
|
|
2989
|
+
Step-based workflows stored as \`<stashDir>/workflows/<name>.md\`.
|
|
2990
|
+
|
|
2991
|
+
\`\`\`sh
|
|
2992
|
+
akm workflow template # Print a starter workflow template
|
|
2993
|
+
akm workflow create ship-release # Scaffold a new workflow asset
|
|
2994
|
+
akm workflow start workflow:ship-release # Start a new run
|
|
2995
|
+
akm workflow next workflow:ship-release # Advance to the next step (or auto-start)
|
|
2996
|
+
akm workflow complete <run-id> # Mark a step complete and advance
|
|
2997
|
+
akm workflow status <run-id> # Show current run status
|
|
2998
|
+
akm workflow resume <run-id> # Resume a blocked or failed run
|
|
2999
|
+
akm workflow list # List all workflow runs
|
|
1683
3000
|
\`\`\`
|
|
1684
3001
|
|
|
1685
3002
|
## Clone
|
|
@@ -1696,6 +3013,47 @@ akm clone "npm:@scope/pkg//script:deploy.sh" # Clone from remote package
|
|
|
1696
3013
|
|
|
1697
3014
|
When \`--dest\` is provided, \`akm init\` is not required first.
|
|
1698
3015
|
|
|
3016
|
+
## Save
|
|
3017
|
+
|
|
3018
|
+
Commit local changes in a git-backed stash. Behaviour adapts automatically:
|
|
3019
|
+
|
|
3020
|
+
- **Not a git repo** — no-op (silent skip)
|
|
3021
|
+
- **Git repo, no remote** — stage and commit only (the default stash always falls here)
|
|
3022
|
+
- **Git repo, has remote, not writable** — stage and commit only
|
|
3023
|
+
- **Git repo, has remote, \`writable: true\`** — stage, commit, and push
|
|
3024
|
+
|
|
3025
|
+
\`\`\`sh
|
|
3026
|
+
akm save # Save primary stash (timestamp message)
|
|
3027
|
+
akm save -m "Add deploy skill" # Save with explicit message
|
|
3028
|
+
akm save my-skills # Save a named writable git stash
|
|
3029
|
+
akm save my-skills -m "Update patterns" # Save named stash with message
|
|
3030
|
+
\`\`\`
|
|
3031
|
+
|
|
3032
|
+
The \`--writable\` flag on \`akm add\` opts a remote git stash into push-on-save:
|
|
3033
|
+
|
|
3034
|
+
\`\`\`sh
|
|
3035
|
+
akm add git@github.com:org/skills.git --provider git --name my-skills --writable
|
|
3036
|
+
\`\`\`
|
|
3037
|
+
|
|
3038
|
+
## Add & Manage Sources
|
|
3039
|
+
|
|
3040
|
+
\`\`\`sh
|
|
3041
|
+
akm add <ref> # Add a source
|
|
3042
|
+
akm add @scope/kit # From npm (managed)
|
|
3043
|
+
akm add owner/repo # From GitHub (managed)
|
|
3044
|
+
akm add ./path/to/local/kit # Local directory
|
|
3045
|
+
akm add git@github.com:org/repo.git --provider git --name my-skills --writable
|
|
3046
|
+
akm enable skills.sh # Enable the skills.sh registry
|
|
3047
|
+
akm disable skills.sh # Disable the skills.sh registry
|
|
3048
|
+
akm enable context-hub # Add/enable the context-hub source
|
|
3049
|
+
akm disable context-hub # Disable the context-hub source
|
|
3050
|
+
akm list # List all sources
|
|
3051
|
+
akm list --kind managed # List managed sources only
|
|
3052
|
+
akm remove <target> # Remove by id, ref, path, or name
|
|
3053
|
+
akm update --all # Update all managed sources
|
|
3054
|
+
akm update <target> --force # Force re-download
|
|
3055
|
+
\`\`\`
|
|
3056
|
+
|
|
1699
3057
|
## Registries
|
|
1700
3058
|
|
|
1701
3059
|
\`\`\`sh
|