akm-cli 0.4.1 → 0.5.0-rc2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { defineCommand, runMain } from "citty";
5
- import { resolveAssetPathFromName } from "./asset-spec";
5
+ import { deriveCanonicalAssetName, resolveAssetPathFromName } from "./asset-spec";
6
6
  import { isWithin, resolveStashDir } from "./common";
7
7
  import { generateBashCompletions, installBashCompletions } from "./completions";
8
8
  import { DEFAULT_CONFIG, getConfigPath, loadConfig, loadUserConfig, saveConfig } from "./config";
@@ -14,18 +14,24 @@ import { assembleInfo } from "./info";
14
14
  import { akmInit } from "./init";
15
15
  import { formatInstallAuditSummary } from "./install-audit";
16
16
  import { akmListSources, akmRemove, akmUpdate } from "./installed-kits";
17
+ import { renderMigrationHelp } from "./migration-help";
17
18
  import { getCacheDir, getDbPath, getDefaultStashDir } from "./paths";
18
19
  import { buildRegistryIndex, writeRegistryIndex } from "./registry-build-index";
19
20
  import { searchRegistry } from "./registry-search";
20
21
  import { checkForUpdate, performUpgrade } from "./self-update";
21
22
  import { akmAdd } from "./stash-add";
22
23
  import { akmClone } from "./stash-clone";
24
+ import { saveGitStash } from "./stash-providers/git";
25
+ import { parseAssetRef } from "./stash-ref";
23
26
  import { akmSearch, parseSearchSource } from "./stash-search";
24
27
  import { akmShowUnified } from "./stash-show";
25
28
  import { addStash } from "./stash-source-manage";
26
29
  import { insertUsageEvent } from "./usage-events";
27
30
  import { pkgVersion } from "./version";
28
31
  import { setQuiet, warn } from "./warn";
32
+ import { createWorkflowAsset, getWorkflowTemplate } from "./workflow-authoring";
33
+ import { hasWorkflowSubcommand, parseWorkflowJsonObject, parseWorkflowStepState, WORKFLOW_STEP_STATES, } from "./workflow-cli";
34
+ import { completeWorkflowStep, getNextWorkflowStep, getWorkflowStatus, listWorkflowRuns, resumeWorkflowRun, startWorkflowRun, } from "./workflow-runs";
29
35
  const OUTPUT_FORMATS = ["json", "yaml", "text", "jsonl"];
30
36
  const DETAIL_LEVELS = ["brief", "normal", "full", "summary"];
31
37
  const NORMAL_DESCRIPTION_LIMIT = 250;
@@ -235,10 +241,27 @@ function shapeShowOutput(result, detail, forAgent = false) {
235
241
  "modelHint",
236
242
  "agent",
237
243
  "parameters",
244
+ "workflowTitle",
245
+ "workflowParameters",
246
+ "steps",
247
+ "keys",
248
+ "comments",
238
249
  ]);
239
250
  }
240
251
  if (detail === "summary") {
241
- return pickFields(result, ["type", "name", "description", "tags", "parameters", "action", "run", "origin"]);
252
+ return pickFields(result, [
253
+ "type",
254
+ "name",
255
+ "description",
256
+ "tags",
257
+ "parameters",
258
+ "workflowTitle",
259
+ "action",
260
+ "run",
261
+ "origin",
262
+ "keys",
263
+ "comments",
264
+ ]);
242
265
  }
243
266
  const base = pickFields(result, [
244
267
  "type",
@@ -254,9 +277,14 @@ function shapeShowOutput(result, detail, forAgent = false) {
254
277
  "modelHint",
255
278
  "agent",
256
279
  "parameters",
280
+ "workflowTitle",
281
+ "workflowParameters",
282
+ "steps",
257
283
  "run",
258
284
  "setup",
259
285
  "cwd",
286
+ "keys",
287
+ "comments",
260
288
  ]);
261
289
  if (detail !== "full") {
262
290
  return base;
@@ -309,10 +337,22 @@ function formatPlain(command, result, detail) {
309
337
  lines.push(`# ${String(r.action)}`);
310
338
  if (r.description)
311
339
  lines.push(`description: ${String(r.description)}`);
340
+ if (r.workflowTitle)
341
+ lines.push(`workflowTitle: ${String(r.workflowTitle)}`);
312
342
  if (r.agent)
313
343
  lines.push(`agent: ${String(r.agent)}`);
314
344
  if (Array.isArray(r.parameters) && r.parameters.length > 0)
315
345
  lines.push(`parameters: ${r.parameters.join(", ")}`);
346
+ if (Array.isArray(r.workflowParameters) && r.workflowParameters.length > 0) {
347
+ lines.push("workflowParameters:");
348
+ for (const parameter of r.workflowParameters) {
349
+ const name = typeof parameter.name === "string" ? parameter.name : "unknown";
350
+ const description = typeof parameter.description === "string" && parameter.description.trim()
351
+ ? `: ${parameter.description}`
352
+ : "";
353
+ lines.push(` - ${name}${description}`);
354
+ }
355
+ }
316
356
  if (r.modelHint != null)
317
357
  lines.push(`modelHint: ${String(r.modelHint)}`);
318
358
  if (r.toolPolicy != null)
@@ -334,6 +374,25 @@ function formatPlain(command, result, detail) {
334
374
  lines.push(`schemaVersion: ${String(r.schemaVersion)}`);
335
375
  }
336
376
  const payloads = [r.content, r.template, r.prompt].filter((value) => value != null).map(String);
377
+ if (Array.isArray(r.steps) && r.steps.length > 0) {
378
+ if (lines.length > 0)
379
+ lines.push("");
380
+ lines.push("steps:");
381
+ for (const [index, step] of r.steps.entries()) {
382
+ const title = typeof step.title === "string" ? step.title : "Untitled step";
383
+ const id = typeof step.id === "string" ? step.id : "unknown";
384
+ lines.push(` ${index + 1}. ${title} [${id}]`);
385
+ if (typeof step.instructions === "string" && step.instructions.trim()) {
386
+ lines.push(` instructions: ${step.instructions.replace(/\n+/g, " ").trim()}`);
387
+ }
388
+ if (Array.isArray(step.completionCriteria) && step.completionCriteria.length > 0) {
389
+ lines.push(" completion:");
390
+ for (const criterion of step.completionCriteria) {
391
+ lines.push(` - ${String(criterion)}`);
392
+ }
393
+ }
394
+ }
395
+ }
337
396
  if (payloads.length > 0) {
338
397
  if (lines.length > 0)
339
398
  lines.push("");
@@ -347,6 +406,47 @@ function formatPlain(command, result, detail) {
347
406
  case "curate": {
348
407
  return formatCuratePlain(r, detail);
349
408
  }
409
+ case "wiki-list": {
410
+ return formatWikiListPlain(r);
411
+ }
412
+ case "wiki-show": {
413
+ return formatWikiShowPlain(r);
414
+ }
415
+ case "wiki-create": {
416
+ return formatWikiCreatePlain(r);
417
+ }
418
+ case "wiki-remove": {
419
+ return formatWikiRemovePlain(r);
420
+ }
421
+ case "wiki-pages": {
422
+ return formatWikiPagesPlain(r);
423
+ }
424
+ case "wiki-stash": {
425
+ return formatWikiStashPlain(r);
426
+ }
427
+ case "wiki-lint": {
428
+ return formatWikiLintPlain(r);
429
+ }
430
+ case "wiki-ingest": {
431
+ return formatWikiIngestPlain(r);
432
+ }
433
+ case "workflow-start":
434
+ case "workflow-status":
435
+ case "workflow-complete": {
436
+ return formatWorkflowStatusPlain(r);
437
+ }
438
+ case "workflow-next": {
439
+ return formatWorkflowNextPlain(r);
440
+ }
441
+ case "workflow-list": {
442
+ return formatWorkflowListPlain(r);
443
+ }
444
+ case "workflow-create": {
445
+ if (r.ref && r.path) {
446
+ return `Created ${String(r.ref)} at ${String(r.path)}`;
447
+ }
448
+ return null;
449
+ }
350
450
  case "list": {
351
451
  const sources = Array.isArray(r.sources) ? r.sources : [];
352
452
  if (sources.length === 0)
@@ -357,7 +457,13 @@ function formatPlain(command, result, detail) {
357
457
  const name = typeof src.name === "string" ? src.name : "unnamed";
358
458
  const ver = typeof src.version === "string" ? ` v${src.version}` : "";
359
459
  const prov = typeof src.provider === "string" ? ` (${src.provider})` : "";
360
- lines.push(`[${kind}] ${name}${ver}${prov}`);
460
+ const flags = [];
461
+ if (src.updatable === true)
462
+ flags.push("updatable");
463
+ if (src.writable === true)
464
+ flags.push("writable");
465
+ const flagText = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
466
+ lines.push(`[${kind}] ${name}${ver}${prov}${flagText}`);
361
467
  }
362
468
  return lines.join("\n");
363
469
  }
@@ -419,6 +525,69 @@ function formatPlain(command, result, detail) {
419
525
  return null; // fall through to YAML
420
526
  }
421
527
  }
528
+ function formatWorkflowListPlain(result) {
529
+ const runs = Array.isArray(result.runs) ? result.runs : [];
530
+ if (runs.length === 0)
531
+ return "No workflow runs found.";
532
+ return runs
533
+ .map((run) => {
534
+ const id = typeof run.id === "string" ? run.id : "unknown";
535
+ const ref = typeof run.workflowRef === "string" ? run.workflowRef : "workflow:unknown";
536
+ const status = typeof run.status === "string" ? run.status : "unknown";
537
+ const currentStep = typeof run.currentStepId === "string" ? ` (current: ${run.currentStepId})` : "";
538
+ return `${id} ${ref} [${status}]${currentStep}`;
539
+ })
540
+ .join("\n");
541
+ }
542
+ function formatWorkflowStatusPlain(result) {
543
+ const run = typeof result.run === "object" && result.run !== null ? result.run : undefined;
544
+ const workflow = typeof result.workflow === "object" && result.workflow !== null
545
+ ? result.workflow
546
+ : undefined;
547
+ if (!run || !workflow)
548
+ return null;
549
+ const lines = [
550
+ `workflow: ${String(workflow.ref ?? "workflow:unknown")}`,
551
+ `run: ${String(run.id ?? "unknown")}`,
552
+ `title: ${String(run.workflowTitle ?? workflow.title ?? "Workflow")}`,
553
+ `status: ${String(run.status ?? "unknown")}`,
554
+ ];
555
+ if (run.currentStepId)
556
+ lines.push(`currentStep: ${String(run.currentStepId)}`);
557
+ const steps = Array.isArray(workflow.steps) ? workflow.steps : [];
558
+ if (steps.length > 0) {
559
+ lines.push("steps:");
560
+ for (const step of steps) {
561
+ const title = typeof step.title === "string" ? step.title : "Untitled step";
562
+ const id = typeof step.id === "string" ? step.id : "unknown";
563
+ const status = typeof step.status === "string" ? step.status : "unknown";
564
+ lines.push(` - ${title} [${id}] (${status})`);
565
+ if (typeof step.notes === "string" && step.notes.trim()) {
566
+ lines.push(` notes: ${step.notes}`);
567
+ }
568
+ }
569
+ }
570
+ return lines.join("\n");
571
+ }
572
+ function formatWorkflowNextPlain(result) {
573
+ const base = formatWorkflowStatusPlain(result);
574
+ const step = typeof result.step === "object" && result.step !== null ? result.step : undefined;
575
+ if (!step)
576
+ return base;
577
+ const lines = base ? [base, "", "next:"] : ["next:"];
578
+ lines.push(` ${String(step.title ?? "Untitled step")} [${String(step.id ?? "unknown")}]`);
579
+ if (typeof step.instructions === "string" && step.instructions.trim()) {
580
+ lines.push(` instructions: ${step.instructions.replace(/\n+/g, " ").trim()}`);
581
+ }
582
+ const completion = Array.isArray(step.completionCriteria) ? step.completionCriteria : [];
583
+ if (completion.length > 0) {
584
+ lines.push(" completion:");
585
+ for (const criterion of completion) {
586
+ lines.push(` - ${String(criterion)}`);
587
+ }
588
+ }
589
+ return lines.join("\n");
590
+ }
422
591
  function formatSearchPlain(r, detail) {
423
592
  const hits = r.hits ?? [];
424
593
  const registryHits = r.registryHits ?? [];
@@ -479,6 +648,98 @@ function formatSearchPlain(r, detail) {
479
648
  }
480
649
  return lines.join("\n").trimEnd();
481
650
  }
651
+ function formatWikiListPlain(r) {
652
+ const wikis = Array.isArray(r.wikis) ? r.wikis : [];
653
+ if (wikis.length === 0)
654
+ return "No wikis. Create one with `akm wiki create <name>`.";
655
+ const lines = ["NAME\tPAGES\tRAWS\tLAST-MODIFIED"];
656
+ for (const w of wikis) {
657
+ const name = typeof w.name === "string" ? w.name : "?";
658
+ const pages = typeof w.pages === "number" ? w.pages : 0;
659
+ const raws = typeof w.raws === "number" ? w.raws : 0;
660
+ const modified = typeof w.lastModified === "string" ? w.lastModified : "-";
661
+ lines.push(`${name}\t${pages}\t${raws}\t${modified}`);
662
+ }
663
+ return lines.join("\n");
664
+ }
665
+ function formatWikiShowPlain(r) {
666
+ const lines = [];
667
+ if (r.name)
668
+ lines.push(`# wiki: ${String(r.name)}`);
669
+ if (r.path)
670
+ lines.push(`path: ${String(r.path)}`);
671
+ if (r.description)
672
+ lines.push(`description: ${String(r.description)}`);
673
+ if (typeof r.pages === "number")
674
+ lines.push(`pages: ${r.pages}`);
675
+ if (typeof r.raws === "number")
676
+ lines.push(`raws: ${r.raws}`);
677
+ if (r.lastModified)
678
+ lines.push(`lastModified: ${String(r.lastModified)}`);
679
+ const recentLog = Array.isArray(r.recentLog) ? r.recentLog : [];
680
+ if (recentLog.length > 0) {
681
+ lines.push("", "recent log:");
682
+ for (const entry of recentLog) {
683
+ lines.push(entry);
684
+ lines.push("");
685
+ }
686
+ }
687
+ return lines.join("\n").trimEnd();
688
+ }
689
+ function formatWikiCreatePlain(r) {
690
+ const created = Array.isArray(r.created) ? r.created : [];
691
+ const skipped = Array.isArray(r.skipped) ? r.skipped : [];
692
+ const lines = [`Created wiki ${String(r.ref ?? r.name)} at ${String(r.path ?? "?")}`];
693
+ if (created.length > 0)
694
+ lines.push(` created: ${created.length} file(s)`);
695
+ if (skipped.length > 0)
696
+ lines.push(` skipped: ${skipped.length} existing file(s)`);
697
+ return lines.join("\n");
698
+ }
699
+ function formatWikiRemovePlain(r) {
700
+ const preserved = r.preservedRaw === true;
701
+ const removed = Array.isArray(r.removed) ? r.removed.length : 0;
702
+ const base = `Removed wiki ${String(r.name ?? "?")} (${removed} path(s))`;
703
+ return preserved ? `${base}; preserved ${String(r.rawPath ?? "raw/")}` : base;
704
+ }
705
+ function formatWikiPagesPlain(r) {
706
+ const pages = Array.isArray(r.pages) ? r.pages : [];
707
+ if (pages.length === 0)
708
+ return `No pages in wiki:${String(r.wiki ?? "?")}.`;
709
+ const lines = [];
710
+ for (const p of pages) {
711
+ const ref = String(p.ref ?? "?");
712
+ const kind = typeof p.pageKind === "string" ? ` [${p.pageKind}]` : "";
713
+ const desc = typeof p.description === "string" && p.description ? ` — ${p.description}` : "";
714
+ lines.push(`${ref}${kind}${desc}`);
715
+ }
716
+ return lines.join("\n");
717
+ }
718
+ function formatWikiStashPlain(r) {
719
+ const slug = String(r.slug ?? "?");
720
+ const pathValue = String(r.path ?? "?");
721
+ return `Stashed ${slug} → ${pathValue}`;
722
+ }
723
+ function formatWikiLintPlain(r) {
724
+ const findings = Array.isArray(r.findings) ? r.findings : [];
725
+ const pagesScanned = typeof r.pagesScanned === "number" ? r.pagesScanned : 0;
726
+ const rawsScanned = typeof r.rawsScanned === "number" ? r.rawsScanned : 0;
727
+ const header = `${findings.length} finding(s) in wiki:${String(r.wiki ?? "?")} (${pagesScanned} page(s), ${rawsScanned} raw(s))`;
728
+ if (findings.length === 0)
729
+ return `${header} — clean.`;
730
+ const lines = [header];
731
+ for (const f of findings) {
732
+ const kind = String(f.kind ?? "?");
733
+ const message = String(f.message ?? "");
734
+ lines.push(`- [${kind}] ${message}`);
735
+ }
736
+ return lines.join("\n");
737
+ }
738
+ function formatWikiIngestPlain(r) {
739
+ if (typeof r.workflow === "string")
740
+ return r.workflow;
741
+ return JSON.stringify(r, null, 2);
742
+ }
482
743
  function formatCuratePlain(r, detail) {
483
744
  const query = typeof r.query === "string" ? r.query : "";
484
745
  const summary = typeof r.summary === "string" ? r.summary : "";
@@ -813,7 +1074,7 @@ const searchCommand = defineCommand({
813
1074
  query: { type: "positional", description: "Search query (omit to list all assets)", required: false, default: "" },
814
1075
  type: {
815
1076
  type: "string",
816
- description: "Asset type filter (e.g. skill, command, agent, knowledge, script, memory, or any).",
1077
+ description: "Asset type filter (skill, command, agent, knowledge, workflow, script, memory, vault, wiki, or any). Use workflow to find step-by-step task assets.",
817
1078
  },
818
1079
  limit: { type: "string", description: "Maximum number of results" },
819
1080
  source: { type: "string", description: "Search source (stash|registry|both)", default: "stash" },
@@ -840,7 +1101,7 @@ const curateCommand = defineCommand({
840
1101
  query: { type: "positional", description: "Task or prompt to curate assets for", required: true },
841
1102
  type: {
842
1103
  type: "string",
843
- description: "Asset type filter (e.g. skill, command, agent, knowledge, script, memory, or any).",
1104
+ description: "Asset type filter (skill, command, agent, knowledge, workflow, script, memory, vault, wiki, or any). Use workflow to curate step-by-step task assets.",
844
1105
  },
845
1106
  limit: { type: "string", description: "Maximum number of curated results", default: "4" },
846
1107
  source: { type: "string", description: "Search source (stash|registry|both)", default: "stash" },
@@ -881,6 +1142,20 @@ const addCommand = defineCommand({
881
1142
  provider: { type: "string", description: "Provider type (e.g. openviking). Required for URL sources." },
882
1143
  options: { type: "string", description: 'Provider options as JSON (e.g. \'{"apiKey":"key"}\').' },
883
1144
  name: { type: "string", description: "Human-friendly name for the source" },
1145
+ writable: {
1146
+ type: "boolean",
1147
+ description: "Mark a git stash as writable so changes can be pushed back",
1148
+ default: false,
1149
+ },
1150
+ trust: {
1151
+ type: "boolean",
1152
+ description: "Bypass install-audit blocking for this add invocation only",
1153
+ default: false,
1154
+ },
1155
+ type: {
1156
+ type: "string",
1157
+ description: "Override asset type for all files in this stash (currently supports: wiki)",
1158
+ },
884
1159
  "max-pages": { type: "string", description: "Maximum pages to crawl for website sources (default: 50)" },
885
1160
  "max-depth": { type: "string", description: "Maximum crawl depth for website sources (default: 3)" },
886
1161
  },
@@ -891,7 +1166,7 @@ const addCommand = defineCommand({
891
1166
  if (ref === CONTEXT_HUB_ALIAS_REF) {
892
1167
  const result = addStash({
893
1168
  target: CONTEXT_HUB_ALIAS_URL,
894
- providerType: "context-hub",
1169
+ providerType: "git",
895
1170
  name: "context-hub",
896
1171
  });
897
1172
  output("stash-add", result);
@@ -922,6 +1197,7 @@ const addCommand = defineCommand({
922
1197
  name: args.name,
923
1198
  providerType: args.provider,
924
1199
  options: parsedOptions,
1200
+ writable: args.writable,
925
1201
  });
926
1202
  output("stash-add", result);
927
1203
  return;
@@ -937,7 +1213,10 @@ const addCommand = defineCommand({
937
1213
  const result = await akmAdd({
938
1214
  ref,
939
1215
  name: args.name,
1216
+ overrideType: args.type,
940
1217
  options: Object.keys(websiteOptions).length > 0 ? websiteOptions : undefined,
1218
+ trustThisInstall: args.trust,
1219
+ writable: args.writable,
941
1220
  });
942
1221
  output("add", result);
943
1222
  });
@@ -1175,6 +1454,91 @@ const configCommand = defineCommand({
1175
1454
  });
1176
1455
  },
1177
1456
  });
1457
+ const saveCommand = defineCommand({
1458
+ meta: {
1459
+ name: "save",
1460
+ description: "Save changes in a git-backed stash: commits (and pushes when writable + remote is configured). No-op for non-git stashes.",
1461
+ },
1462
+ args: {
1463
+ name: {
1464
+ type: "positional",
1465
+ description: "Name of the git stash to save (default: primary stash directory)",
1466
+ required: false,
1467
+ },
1468
+ message: {
1469
+ type: "string",
1470
+ alias: "m",
1471
+ description: "Commit message (default: timestamp)",
1472
+ },
1473
+ },
1474
+ async run({ args }) {
1475
+ await runWithJsonErrors(async () => {
1476
+ // Fix: citty can consume `--format json` (space-separated) as the
1477
+ // positional `name` argument (e.g. `akm save --format json` parses
1478
+ // name="json"). Detect the mis-parse by checking argv order — only
1479
+ // treat the positional as consumed by --format when --format appears
1480
+ // before any standalone occurrence of the same value in the save
1481
+ // subcommand's argv slice. This preserves legitimate invocations
1482
+ // like `akm save json --format json`.
1483
+ const parsedFormat = parseFlagValue("--format");
1484
+ const effectiveName = args.name !== undefined &&
1485
+ parsedFormat !== undefined &&
1486
+ args.name === parsedFormat &&
1487
+ wasFormatValueConsumedAsName(args.name, parsedFormat)
1488
+ ? undefined
1489
+ : args.name;
1490
+ let writable;
1491
+ if (!effectiveName) {
1492
+ // Primary stash — honour the root-level writable flag from config.
1493
+ const cfg = loadConfig();
1494
+ writable = cfg.writable === true ? true : undefined;
1495
+ }
1496
+ const result = saveGitStash(effectiveName, args.message, writable);
1497
+ output("save", result);
1498
+ });
1499
+ },
1500
+ });
1501
+ /**
1502
+ * Detect whether `--format <value>` was consumed by citty as the optional
1503
+ * `name` positional of `akm save`. Returns true only when `--format` appears
1504
+ * in the save subcommand's argv slice AND the candidate name does NOT
1505
+ * appear as a standalone positional elsewhere (before or after the flag).
1506
+ *
1507
+ * This keeps `akm save json --format json` routing `json` as the stash name,
1508
+ * while `akm save --format json` (no separate positional) is treated as a
1509
+ * primary-stash save.
1510
+ */
1511
+ function wasFormatValueConsumedAsName(name, formatValue) {
1512
+ const argv = process.argv.slice(2);
1513
+ const saveIndex = argv.indexOf("save");
1514
+ const tokens = saveIndex >= 0 ? argv.slice(saveIndex + 1) : argv;
1515
+ let formatIndex = -1;
1516
+ let formatConsumesNextToken = false;
1517
+ for (let i = 0; i < tokens.length; i += 1) {
1518
+ const token = tokens[i];
1519
+ if (token === "--format") {
1520
+ formatIndex = i;
1521
+ formatConsumesNextToken = true;
1522
+ break;
1523
+ }
1524
+ if (token === `--format=${formatValue}`) {
1525
+ formatIndex = i;
1526
+ break;
1527
+ }
1528
+ }
1529
+ if (formatIndex === -1)
1530
+ return false;
1531
+ // If the name appears as a standalone token before --format, it's the
1532
+ // real positional and --format did not consume it.
1533
+ if (tokens.slice(0, formatIndex).includes(name))
1534
+ return false;
1535
+ // If --format has a space-separated value, skip past the value token
1536
+ // when scanning after the flag; otherwise start right after the flag.
1537
+ const firstTokenAfterFormat = formatIndex + (formatConsumesNextToken ? 2 : 1);
1538
+ if (tokens.slice(firstTokenAfterFormat).includes(name))
1539
+ return false;
1540
+ return true;
1541
+ }
1178
1542
  const cloneCommand = defineCommand({
1179
1543
  meta: {
1180
1544
  name: "clone",
@@ -1449,6 +1813,206 @@ function writeMarkdownAsset(options) {
1449
1813
  stashDir,
1450
1814
  };
1451
1815
  }
1816
+ const workflowStartCommand = defineCommand({
1817
+ meta: {
1818
+ name: "start",
1819
+ description: "Start a new workflow run",
1820
+ },
1821
+ args: {
1822
+ ref: { type: "positional", description: "Workflow ref (workflow:<name>)", required: true },
1823
+ params: { type: "string", description: "Workflow parameters as a JSON object" },
1824
+ },
1825
+ async run({ args }) {
1826
+ await runWithJsonErrors(async () => {
1827
+ const result = await startWorkflowRun(args.ref, parseWorkflowJsonObject(args.params, "--params"));
1828
+ output("workflow-start", result);
1829
+ });
1830
+ },
1831
+ });
1832
+ const workflowNextCommand = defineCommand({
1833
+ meta: {
1834
+ name: "next",
1835
+ description: "Show the next actionable workflow step, auto-starting a run when passed a workflow ref",
1836
+ },
1837
+ args: {
1838
+ target: { type: "positional", description: "Workflow run id or workflow ref", required: true },
1839
+ params: { type: "string", description: "Workflow parameters as a JSON object (only for auto-started runs)" },
1840
+ },
1841
+ async run({ args }) {
1842
+ await runWithJsonErrors(async () => {
1843
+ const parsedParams = args.params ? parseWorkflowJsonObject(args.params, "--params") : undefined;
1844
+ const result = await getNextWorkflowStep(args.target, parsedParams);
1845
+ output("workflow-next", result);
1846
+ });
1847
+ },
1848
+ });
1849
+ const workflowCompleteCommand = defineCommand({
1850
+ meta: {
1851
+ name: "complete",
1852
+ description: "Update a workflow step state and persist notes/evidence",
1853
+ },
1854
+ args: {
1855
+ runId: { type: "positional", description: "Workflow run id", required: true },
1856
+ step: { type: "string", description: "Workflow step id", required: true },
1857
+ state: {
1858
+ type: "string",
1859
+ description: `Step state (default: completed). One of: ${WORKFLOW_STEP_STATES.join(", ")}.`,
1860
+ },
1861
+ notes: { type: "string", description: "Notes for the completed step" },
1862
+ evidence: { type: "string", description: "Evidence JSON object for the step" },
1863
+ },
1864
+ async run({ args }) {
1865
+ await runWithJsonErrors(async () => {
1866
+ const result = completeWorkflowStep({
1867
+ runId: args.runId,
1868
+ stepId: args.step,
1869
+ status: parseWorkflowStepState(args.state),
1870
+ notes: args.notes,
1871
+ evidence: args.evidence ? parseWorkflowJsonObject(args.evidence, "--evidence") : undefined,
1872
+ });
1873
+ output("workflow-complete", result);
1874
+ });
1875
+ },
1876
+ });
1877
+ const workflowStatusCommand = defineCommand({
1878
+ meta: {
1879
+ name: "status",
1880
+ description: "Show full workflow run state for review or resume",
1881
+ },
1882
+ args: {
1883
+ target: { type: "positional", description: "Workflow run id or workflow ref (workflow:<name>)", required: true },
1884
+ },
1885
+ run({ args }) {
1886
+ return runWithJsonErrors(() => {
1887
+ const target = args.target;
1888
+ // Check if target looks like a workflow ref
1889
+ const parsed = (() => {
1890
+ try {
1891
+ return parseAssetRef(target);
1892
+ }
1893
+ catch {
1894
+ return null;
1895
+ }
1896
+ })();
1897
+ if (parsed?.type === "workflow") {
1898
+ const ref = `${parsed.origin ? `${parsed.origin}//` : ""}workflow:${parsed.name}`;
1899
+ const { runs } = listWorkflowRuns({ workflowRef: ref });
1900
+ if (runs.length === 0) {
1901
+ throw new NotFoundError(`No workflow runs found for ${ref}`);
1902
+ }
1903
+ const mostRecent = runs[0];
1904
+ if (!mostRecent)
1905
+ throw new NotFoundError(`No workflow runs found for ${ref}`);
1906
+ const result = getWorkflowStatus(mostRecent.id);
1907
+ output("workflow-status", result);
1908
+ }
1909
+ else {
1910
+ const result = getWorkflowStatus(target);
1911
+ output("workflow-status", result);
1912
+ }
1913
+ });
1914
+ },
1915
+ });
1916
+ const workflowListCommand = defineCommand({
1917
+ meta: {
1918
+ name: "list",
1919
+ description: "List workflow runs",
1920
+ },
1921
+ args: {
1922
+ ref: { type: "string", description: "Filter to one workflow ref" },
1923
+ active: { type: "boolean", description: "Only show active runs", default: false },
1924
+ },
1925
+ run({ args }) {
1926
+ return runWithJsonErrors(() => {
1927
+ const result = listWorkflowRuns({ workflowRef: args.ref, activeOnly: args.active });
1928
+ output("workflow-list", result);
1929
+ });
1930
+ },
1931
+ });
1932
+ const workflowCreateCommand = defineCommand({
1933
+ meta: {
1934
+ name: "create",
1935
+ description: "Create a workflow markdown document in the working stash",
1936
+ },
1937
+ args: {
1938
+ name: { type: "positional", description: "Workflow name", required: true },
1939
+ from: { type: "string", description: "Import and validate markdown from an existing file" },
1940
+ force: {
1941
+ type: "boolean",
1942
+ description: "Overwrite an existing workflow (requires --from or --reset)",
1943
+ default: false,
1944
+ },
1945
+ reset: {
1946
+ type: "boolean",
1947
+ description: "Explicitly replace an existing workflow with a fresh template (use with --force)",
1948
+ default: false,
1949
+ },
1950
+ },
1951
+ run({ args }) {
1952
+ return runWithJsonErrors(() => {
1953
+ const namePattern = /^[a-z0-9][a-z0-9._/-]*$/;
1954
+ if (!namePattern.test(args.name)) {
1955
+ throw new UsageError("Workflow name must start with a lowercase letter or digit and contain only lowercase letters, digits, hyphens, dots, underscores, and slashes.");
1956
+ }
1957
+ if (args.force && !args.from && !args.reset) {
1958
+ throw new UsageError("Refusing to overwrite with template: pass --from <file> to replace content, or --reset to explicitly replace with a fresh template.");
1959
+ }
1960
+ const result = createWorkflowAsset({
1961
+ name: args.name,
1962
+ from: args.from,
1963
+ force: args.force,
1964
+ });
1965
+ output("workflow-create", { ok: true, ...result });
1966
+ });
1967
+ },
1968
+ });
1969
+ const workflowTemplateCommand = defineCommand({
1970
+ meta: {
1971
+ name: "template",
1972
+ description: "Print a valid workflow markdown template",
1973
+ },
1974
+ run() {
1975
+ process.stdout.write(getWorkflowTemplate());
1976
+ },
1977
+ });
1978
+ const workflowResumeCommand = defineCommand({
1979
+ meta: {
1980
+ name: "resume",
1981
+ description: "Resume a blocked or failed workflow run, flipping it back to active",
1982
+ },
1983
+ args: {
1984
+ runId: { type: "positional", description: "Workflow run id", required: true },
1985
+ },
1986
+ run({ args }) {
1987
+ return runWithJsonErrors(() => {
1988
+ const result = resumeWorkflowRun(args.runId);
1989
+ output("workflow-run", result);
1990
+ });
1991
+ },
1992
+ });
1993
+ const workflowCommand = defineCommand({
1994
+ meta: {
1995
+ name: "workflow",
1996
+ description: "Author, inspect, and execute step-by-step workflow assets",
1997
+ },
1998
+ subCommands: {
1999
+ start: workflowStartCommand,
2000
+ next: workflowNextCommand,
2001
+ complete: workflowCompleteCommand,
2002
+ status: workflowStatusCommand,
2003
+ list: workflowListCommand,
2004
+ create: workflowCreateCommand,
2005
+ template: workflowTemplateCommand,
2006
+ resume: workflowResumeCommand,
2007
+ },
2008
+ run({ args }) {
2009
+ return runWithJsonErrors(() => {
2010
+ if (hasWorkflowSubcommand(args))
2011
+ return;
2012
+ output("workflow-list", listWorkflowRuns({ activeOnly: true }));
2013
+ });
2014
+ },
2015
+ });
1452
2016
  const rememberCommand = defineCommand({
1453
2017
  meta: {
1454
2018
  name: "remember",
@@ -1504,8 +2068,8 @@ const importKnowledgeCommand = defineCommand({
1504
2068
  default: false,
1505
2069
  },
1506
2070
  },
1507
- run({ args }) {
1508
- return runWithJsonErrors(() => {
2071
+ async run({ args }) {
2072
+ return runWithJsonErrors(async () => {
1509
2073
  const { content, preferredName } = readKnowledgeContent(args.source);
1510
2074
  const result = writeMarkdownAsset({
1511
2075
  type: "knowledge",
@@ -1532,6 +2096,30 @@ const hintsCommand = defineCommand({
1532
2096
  process.stdout.write(loadHints(detail));
1533
2097
  },
1534
2098
  });
2099
+ const helpCommand = defineCommand({
2100
+ meta: {
2101
+ name: "help",
2102
+ description: "Print focused help topics such as migration guidance for a release",
2103
+ },
2104
+ subCommands: {
2105
+ migrate: defineCommand({
2106
+ meta: {
2107
+ name: "migrate",
2108
+ description: "Print release notes and migration guidance for a version",
2109
+ },
2110
+ args: {
2111
+ version: {
2112
+ type: "positional",
2113
+ description: "Version to review (for example 0.5.0, v0.5.0, or latest)",
2114
+ required: true,
2115
+ },
2116
+ },
2117
+ run({ args }) {
2118
+ process.stdout.write(renderMigrationHelp(args.version));
2119
+ },
2120
+ }),
2121
+ },
2122
+ });
1535
2123
  const completionsCommand = defineCommand({
1536
2124
  meta: {
1537
2125
  name: "completions",
@@ -1641,6 +2229,429 @@ const disableCommand = defineCommand({
1641
2229
  });
1642
2230
  },
1643
2231
  });
2232
+ // ── vault ───────────────────────────────────────────────────────────────────
2233
+ //
2234
+ // `akm vault` manages secrets stored in `.env` files under the vaults/
2235
+ // asset directory. Values are NEVER written to stdout. `vault load` is
2236
+ // the only value-emitting path: it parses the vault with dotenv, writes
2237
+ // a safely-escaped shell script to a mode-0600 temp file, and emits only
2238
+ // `. <temp>; rm -f <temp>` on stdout for `eval`. The shell reads values
2239
+ // from the temp file — they never transit through akm's stdout.
2240
+ function resolveVaultPath(ref) {
2241
+ const stashDir = resolveStashDir({ readOnly: true });
2242
+ const parsed = parseAssetRef(ref.includes(":") ? ref : `vault:${ref}`);
2243
+ if (parsed.type !== "vault") {
2244
+ throw new UsageError(`Expected a vault ref (vault:<name>); got "${ref}".`);
2245
+ }
2246
+ const typeRoot = path.join(stashDir, "vaults");
2247
+ const absPath = resolveAssetPathFromName("vault", typeRoot, parsed.name);
2248
+ return { name: parsed.name, absPath };
2249
+ }
2250
+ /**
2251
+ * Walk `vaults/` recursively and return one entry per `.env` file, using the
2252
+ * vault asset spec's canonical-name logic so listing matches what the
2253
+ * matcher/asset-spec actually resolves (e.g. `vaults/team/prod.env` →
2254
+ * `vault:team/prod`, `vaults/team/.env` → `vault:team/default`).
2255
+ */
2256
+ function listVaultsRecursive(listKeysFn) {
2257
+ const stashDir = resolveStashDir({ readOnly: true });
2258
+ const vaultsDir = path.join(stashDir, "vaults");
2259
+ const result = [];
2260
+ if (!fs.existsSync(vaultsDir))
2261
+ return result;
2262
+ const walk = (dir) => {
2263
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
2264
+ const full = path.join(dir, entry.name);
2265
+ if (entry.isDirectory()) {
2266
+ walk(full);
2267
+ continue;
2268
+ }
2269
+ if (!entry.isFile())
2270
+ continue;
2271
+ if (entry.name !== ".env" && !entry.name.endsWith(".env"))
2272
+ continue;
2273
+ const canonical = deriveCanonicalAssetName("vault", vaultsDir, full);
2274
+ if (!canonical)
2275
+ continue;
2276
+ const { keys } = listKeysFn(full);
2277
+ result.push({ ref: `vault:${canonical}`, path: full, keyCount: keys.length });
2278
+ }
2279
+ };
2280
+ walk(vaultsDir);
2281
+ return result;
2282
+ }
2283
+ const vaultListCommand = defineCommand({
2284
+ meta: { name: "list", description: "List vaults, or list keys (no values) inside one vault" },
2285
+ args: {
2286
+ ref: { type: "positional", description: "Optional vault ref (e.g. vault:prod or just prod)", required: false },
2287
+ },
2288
+ run({ args }) {
2289
+ return runWithJsonErrors(async () => {
2290
+ const { listKeys } = await import("./vault.js");
2291
+ if (args.ref) {
2292
+ const { name, absPath } = resolveVaultPath(args.ref);
2293
+ if (!fs.existsSync(absPath)) {
2294
+ throw new NotFoundError(`Vault not found: vault:${name}`);
2295
+ }
2296
+ const { keys, comments } = listKeys(absPath);
2297
+ output("vault-list", { ref: `vault:${name}`, path: absPath, keys, comments });
2298
+ return;
2299
+ }
2300
+ const vaults = listVaultsRecursive(listKeys);
2301
+ output("vault-list", { vaults });
2302
+ });
2303
+ },
2304
+ });
2305
+ const vaultCreateCommand = defineCommand({
2306
+ meta: { name: "create", description: "Create an empty vault file (no-op if it already exists)" },
2307
+ args: {
2308
+ name: { type: "positional", description: "Vault name (e.g. prod) — file becomes <name>.env", required: true },
2309
+ },
2310
+ run({ args }) {
2311
+ return runWithJsonErrors(async () => {
2312
+ const { createVault } = await import("./vault.js");
2313
+ const { name, absPath } = resolveVaultPath(args.name);
2314
+ createVault(absPath);
2315
+ output("vault-create", { ref: `vault:${name}`, path: absPath });
2316
+ });
2317
+ },
2318
+ });
2319
+ const vaultSetCommand = defineCommand({
2320
+ meta: {
2321
+ name: "set",
2322
+ 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".',
2323
+ },
2324
+ args: {
2325
+ ref: { type: "positional", description: "Vault ref (e.g. vault:prod or just prod)", required: true },
2326
+ key: { type: "positional", description: "Key name (e.g. DB_URL) or KEY=VALUE combined form", required: true },
2327
+ value: {
2328
+ type: "positional",
2329
+ description: "Value to store (omit when using KEY=VALUE combined form)",
2330
+ required: false,
2331
+ },
2332
+ comment: { type: "string", description: "Optional comment written above the key line", required: false },
2333
+ },
2334
+ run({ args }) {
2335
+ return runWithJsonErrors(async () => {
2336
+ const { setKey } = await import("./vault.js");
2337
+ const { name, absPath } = resolveVaultPath(args.ref);
2338
+ let realKey;
2339
+ let realValue;
2340
+ if ((args.value === undefined || args.value === "") && args.key.includes("=")) {
2341
+ const eqIdx = args.key.indexOf("=");
2342
+ realKey = args.key.slice(0, eqIdx);
2343
+ realValue = args.key.slice(eqIdx + 1);
2344
+ }
2345
+ else {
2346
+ realKey = args.key;
2347
+ realValue = args.value ?? "";
2348
+ }
2349
+ setKey(absPath, realKey, realValue, args.comment);
2350
+ output("vault-set", { ref: `vault:${name}`, key: realKey, path: absPath });
2351
+ });
2352
+ },
2353
+ });
2354
+ const vaultUnsetCommand = defineCommand({
2355
+ meta: { name: "unset", description: "Remove a key from a vault" },
2356
+ args: {
2357
+ ref: { type: "positional", description: "Vault ref", required: true },
2358
+ key: { type: "positional", description: "Key name to remove", required: true },
2359
+ },
2360
+ run({ args }) {
2361
+ return runWithJsonErrors(async () => {
2362
+ const { unsetKey } = await import("./vault.js");
2363
+ const { name, absPath } = resolveVaultPath(args.ref);
2364
+ if (!fs.existsSync(absPath)) {
2365
+ throw new NotFoundError(`Vault not found: vault:${name}`);
2366
+ }
2367
+ const removed = unsetKey(absPath, args.key);
2368
+ output("vault-unset", { ref: `vault:${name}`, key: args.key, removed, path: absPath });
2369
+ });
2370
+ },
2371
+ });
2372
+ const vaultLoadCommand = defineCommand({
2373
+ meta: {
2374
+ name: "load",
2375
+ 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.',
2376
+ },
2377
+ args: {
2378
+ ref: { type: "positional", description: "Vault ref", required: true },
2379
+ },
2380
+ async run({ args }) {
2381
+ return runWithJsonErrors(async () => {
2382
+ // This command deliberately bypasses output()/JSON shaping. Its stdout
2383
+ // is a shell snippet intended for `eval`, not structured output.
2384
+ const { name, absPath } = resolveVaultPath(args.ref);
2385
+ if (!fs.existsSync(absPath)) {
2386
+ throw new NotFoundError(`Vault not found: vault:${name}`);
2387
+ }
2388
+ const { buildShellExportScript } = await import("./vault.js");
2389
+ const crypto = await import("node:crypto");
2390
+ const os = await import("node:os");
2391
+ // Parse via dotenv (no expansion, no code execution) and build a
2392
+ // script of literal `export KEY='value'` lines with `'\''` escaping.
2393
+ // Sourcing this is safe even if the raw vault file contained shell
2394
+ // metacharacters like $, backticks, or $(...).
2395
+ const script = buildShellExportScript(absPath);
2396
+ // Write to a mode-0600 temp file the shell can source.
2397
+ const tmpPath = path.join(os.tmpdir(), `akm-vault-${crypto.randomBytes(12).toString("hex")}.sh`);
2398
+ fs.writeFileSync(tmpPath, script, { mode: 0o600, encoding: "utf8" });
2399
+ try {
2400
+ fs.chmodSync(tmpPath, 0o600);
2401
+ }
2402
+ catch {
2403
+ /* best-effort on platforms without chmod */
2404
+ }
2405
+ const quotedTmp = `'${tmpPath.replace(/'/g, "'\\''")}'`;
2406
+ // Emit: source the temp file, then remove it — values reach bash only
2407
+ // via the temp file (mode 0600), never via akm's stdout.
2408
+ process.stdout.write(`. ${quotedTmp}; rm -f ${quotedTmp}\n`);
2409
+ });
2410
+ },
2411
+ });
2412
+ const vaultShowCommand = defineCommand({
2413
+ meta: { name: "show", description: "Show keys (no values) inside a vault — alias for `vault list <ref>`" },
2414
+ args: {
2415
+ ref: { type: "positional", description: "Vault ref (e.g. vault:prod or just prod)", required: true },
2416
+ },
2417
+ run({ args }) {
2418
+ return runWithJsonErrors(async () => {
2419
+ const { listKeys } = await import("./vault.js");
2420
+ const { name, absPath } = resolveVaultPath(args.ref);
2421
+ if (!fs.existsSync(absPath)) {
2422
+ throw new NotFoundError(`Vault not found: vault:${name}`);
2423
+ }
2424
+ const { keys, comments } = listKeys(absPath);
2425
+ output("vault-list", { ref: `vault:${name}`, path: absPath, keys, comments });
2426
+ });
2427
+ },
2428
+ });
2429
+ const vaultCommand = defineCommand({
2430
+ meta: {
2431
+ name: "vault",
2432
+ description: "Manage secret vaults (.env files). Lists keys + comments only — values never returned in structured output.",
2433
+ },
2434
+ subCommands: {
2435
+ list: vaultListCommand,
2436
+ show: vaultShowCommand,
2437
+ create: vaultCreateCommand,
2438
+ set: vaultSetCommand,
2439
+ unset: vaultUnsetCommand,
2440
+ load: vaultLoadCommand,
2441
+ },
2442
+ run({ args }) {
2443
+ return runWithJsonErrors(async () => {
2444
+ if (hasVaultSubcommand(args))
2445
+ return;
2446
+ // Default action: list all vaults
2447
+ const { listKeys } = await import("./vault.js");
2448
+ output("vault-list", { vaults: listVaultsRecursive(listKeys) });
2449
+ });
2450
+ },
2451
+ });
2452
+ // ── Wiki subcommands ─────────────────────────────────────────────────────────
2453
+ const wikiCreateCommand = defineCommand({
2454
+ meta: { name: "create", description: "Scaffold a new wiki under <stashDir>/wikis/<name>/" },
2455
+ args: {
2456
+ name: { type: "positional", description: "Wiki name (lowercase, digits, hyphens)", required: true },
2457
+ },
2458
+ run({ args }) {
2459
+ return runWithJsonErrors(async () => {
2460
+ const { createWiki } = await import("./wiki.js");
2461
+ const stashDir = resolveStashDir();
2462
+ const result = createWiki(stashDir, args.name);
2463
+ output("wiki-create", result);
2464
+ });
2465
+ },
2466
+ });
2467
+ const wikiListCommand = defineCommand({
2468
+ meta: { name: "list", description: "List wikis with page/raw counts and last-modified timestamps" },
2469
+ run() {
2470
+ return runWithJsonErrors(async () => {
2471
+ const { listWikis } = await import("./wiki.js");
2472
+ const stashDir = resolveStashDir();
2473
+ const wikis = listWikis(stashDir);
2474
+ output("wiki-list", { wikis });
2475
+ });
2476
+ },
2477
+ });
2478
+ const wikiShowCommand = defineCommand({
2479
+ meta: { name: "show", description: "Show a wiki's path, description, counts, and last 3 log entries" },
2480
+ args: {
2481
+ name: { type: "positional", description: "Wiki name", required: true },
2482
+ },
2483
+ run({ args }) {
2484
+ return runWithJsonErrors(async () => {
2485
+ const { showWiki } = await import("./wiki.js");
2486
+ const stashDir = resolveStashDir();
2487
+ const result = showWiki(stashDir, args.name);
2488
+ output("wiki-show", result);
2489
+ });
2490
+ },
2491
+ });
2492
+ const wikiRemoveCommand = defineCommand({
2493
+ meta: {
2494
+ name: "remove",
2495
+ description: "Remove a wiki. Preserves raw/ by default; pass --with-sources to also delete raw/",
2496
+ },
2497
+ args: {
2498
+ name: { type: "positional", description: "Wiki name", required: true },
2499
+ force: {
2500
+ type: "boolean",
2501
+ description: "Remove without prompting (required in non-interactive shells)",
2502
+ default: false,
2503
+ },
2504
+ "with-sources": {
2505
+ type: "boolean",
2506
+ description: "Also delete the raw/ directory (immutable ingested sources)",
2507
+ default: false,
2508
+ },
2509
+ },
2510
+ run({ args }) {
2511
+ return runWithJsonErrors(async () => {
2512
+ if (!args.force) {
2513
+ throw new UsageError("Refusing to remove without --force. Pass `--force` to confirm.");
2514
+ }
2515
+ const withSources = Boolean(args["with-sources"]);
2516
+ const { removeWiki } = await import("./wiki.js");
2517
+ const stashDir = resolveStashDir();
2518
+ const result = removeWiki(stashDir, args.name, { withSources });
2519
+ output("wiki-remove", result);
2520
+ });
2521
+ },
2522
+ });
2523
+ const wikiPagesCommand = defineCommand({
2524
+ meta: {
2525
+ name: "pages",
2526
+ description: "List wiki pages (ref + frontmatter description), excluding schema/index/log/raw",
2527
+ },
2528
+ args: {
2529
+ name: { type: "positional", description: "Wiki name", required: true },
2530
+ },
2531
+ run({ args }) {
2532
+ return runWithJsonErrors(async () => {
2533
+ const { listPages } = await import("./wiki.js");
2534
+ const stashDir = resolveStashDir();
2535
+ const pages = listPages(stashDir, args.name);
2536
+ output("wiki-pages", { wiki: args.name, pages });
2537
+ });
2538
+ },
2539
+ });
2540
+ const wikiSearchCommand = defineCommand({
2541
+ meta: {
2542
+ name: "search",
2543
+ description: "Search wiki pages within a single wiki (scoped wrapper over `akm search --type wiki`; excludes raw/schema/index/log)",
2544
+ },
2545
+ args: {
2546
+ name: { type: "positional", description: "Wiki name", required: true },
2547
+ query: { type: "positional", description: "Search query", required: true },
2548
+ limit: { type: "string", description: "Max hits (default 20)", required: false },
2549
+ },
2550
+ run({ args }) {
2551
+ return runWithJsonErrors(async () => {
2552
+ const { searchInWiki } = await import("./wiki.js");
2553
+ const stashDir = resolveStashDir();
2554
+ const wikiDir = path.join(stashDir, "wikis", args.name);
2555
+ if (!fs.existsSync(wikiDir)) {
2556
+ throw new NotFoundError(`Wiki not found: ${args.name}`);
2557
+ }
2558
+ const parsedLimit = args.limit ? Number(args.limit) : undefined;
2559
+ const limit = typeof parsedLimit === "number" && Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : undefined;
2560
+ const response = await searchInWiki({ stashDir, wikiName: args.name, query: args.query, limit });
2561
+ output("search", response);
2562
+ });
2563
+ },
2564
+ });
2565
+ const wikiStashCommand = defineCommand({
2566
+ meta: {
2567
+ name: "stash",
2568
+ description: "Copy a source into wikis/<name>/raw/<slug>.md with frontmatter. Source may be a file path or '-' for stdin.",
2569
+ },
2570
+ args: {
2571
+ name: { type: "positional", description: "Wiki name", required: true },
2572
+ source: { type: "positional", description: "Source file path, or '-' to read from stdin", required: true },
2573
+ as: { type: "string", description: "Preferred slug base (defaults to source filename or first-line slug)" },
2574
+ },
2575
+ run({ args }) {
2576
+ return runWithJsonErrors(async () => {
2577
+ const { stashRaw } = await import("./wiki.js");
2578
+ const { content, preferredName } = readKnowledgeContent(args.source);
2579
+ const stashDir = resolveStashDir();
2580
+ const result = stashRaw({
2581
+ stashDir,
2582
+ wikiName: args.name,
2583
+ content,
2584
+ preferredName: args.as ?? preferredName,
2585
+ explicitSlug: args.as !== undefined,
2586
+ });
2587
+ output("wiki-stash", { ok: true, wiki: args.name, source: args.source, ...result });
2588
+ });
2589
+ },
2590
+ });
2591
+ const wikiLintCommand = defineCommand({
2592
+ meta: {
2593
+ name: "lint",
2594
+ description: "Structural lint for a wiki: orphans, broken xrefs, missing descriptions, uncited raws, stale index",
2595
+ },
2596
+ args: {
2597
+ name: { type: "positional", description: "Wiki name", required: true },
2598
+ },
2599
+ async run({ args }) {
2600
+ let findingCount = 0;
2601
+ await runWithJsonErrors(async () => {
2602
+ const { lintWiki } = await import("./wiki.js");
2603
+ const stashDir = resolveStashDir();
2604
+ const report = lintWiki(stashDir, args.name);
2605
+ output("wiki-lint", report);
2606
+ findingCount = report.findings.length;
2607
+ });
2608
+ if (findingCount > 0)
2609
+ process.exit(1); // EXIT_GENERAL
2610
+ },
2611
+ });
2612
+ const wikiIngestCommand = defineCommand({
2613
+ meta: {
2614
+ name: "ingest",
2615
+ description: "Print the ingest workflow for this wiki. Does not perform the ingest; instructs the agent to.",
2616
+ },
2617
+ args: {
2618
+ name: { type: "positional", description: "Wiki name", required: true },
2619
+ },
2620
+ run({ args }) {
2621
+ return runWithJsonErrors(async () => {
2622
+ const { buildIngestWorkflow } = await import("./wiki.js");
2623
+ const stashDir = resolveStashDir();
2624
+ const result = buildIngestWorkflow(stashDir, args.name);
2625
+ output("wiki-ingest", result);
2626
+ });
2627
+ },
2628
+ });
2629
+ const wikiCommand = defineCommand({
2630
+ meta: {
2631
+ name: "wiki",
2632
+ description: "Manage multiple markdown wikis (Karpathy-style). akm surfaces (lifecycle, raw/, lint, index); the agent writes pages.",
2633
+ },
2634
+ subCommands: {
2635
+ create: wikiCreateCommand,
2636
+ list: wikiListCommand,
2637
+ show: wikiShowCommand,
2638
+ remove: wikiRemoveCommand,
2639
+ pages: wikiPagesCommand,
2640
+ search: wikiSearchCommand,
2641
+ stash: wikiStashCommand,
2642
+ lint: wikiLintCommand,
2643
+ ingest: wikiIngestCommand,
2644
+ },
2645
+ run({ args }) {
2646
+ return runWithJsonErrors(async () => {
2647
+ if (hasWikiSubcommand(args))
2648
+ return;
2649
+ // Default action: list wikis
2650
+ const { listWikis } = await import("./wiki.js");
2651
+ output("wiki-list", { wikis: listWikis(resolveStashDir()) });
2652
+ });
2653
+ },
2654
+ });
1644
2655
  const main = defineCommand({
1645
2656
  meta: {
1646
2657
  name: "akm",
@@ -1648,8 +2659,8 @@ const main = defineCommand({
1648
2659
  description: "Agent Kit Manager — search, show, and manage assets from your stash.",
1649
2660
  },
1650
2661
  args: {
1651
- format: { type: "string", description: "Output format (json|text|yaml)" },
1652
- detail: { type: "string", description: "Detail level (brief|normal|full)" },
2662
+ format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
2663
+ detail: { type: "string", description: "Detail level (brief|normal|full|summary)" },
1653
2664
  quiet: { type: "boolean", alias: "q", description: "Suppress stderr warnings", default: false },
1654
2665
  },
1655
2666
  subCommands: {
@@ -1665,19 +2676,26 @@ const main = defineCommand({
1665
2676
  search: searchCommand,
1666
2677
  curate: curateCommand,
1667
2678
  show: showCommand,
2679
+ workflow: workflowCommand,
1668
2680
  remember: rememberCommand,
1669
2681
  import: importKnowledgeCommand,
2682
+ save: saveCommand,
1670
2683
  clone: cloneCommand,
1671
2684
  registry: registryCommand,
1672
2685
  config: configCommand,
1673
2686
  enable: enableCommand,
1674
2687
  disable: disableCommand,
1675
2688
  feedback: feedbackCommand,
2689
+ help: helpCommand,
1676
2690
  hints: hintsCommand,
1677
2691
  completions: completionsCommand,
2692
+ vault: vaultCommand,
2693
+ wiki: wikiCommand,
1678
2694
  },
1679
2695
  });
1680
2696
  const CONFIG_SUBCOMMAND_SET = new Set(["path", "list", "get", "set", "unset"]);
2697
+ const VAULT_SUBCOMMAND_SET = new Set(["list", "show", "create", "set", "unset", "load"]);
2698
+ const WIKI_SUBCOMMAND_SET = new Set(["create", "list", "show", "remove", "pages", "search", "stash", "lint", "ingest"]);
1681
2699
  const SHOW_VIEW_MODES = new Set(["toc", "frontmatter", "full", "section", "lines"]);
1682
2700
  // citty reads process.argv directly and does not accept a custom argv array,
1683
2701
  // so we must replace process.argv with the normalized version before runMain.
@@ -1738,6 +2756,14 @@ function hasConfigSubcommand(args) {
1738
2756
  const command = Array.isArray(args._) ? args._[0] : undefined;
1739
2757
  return typeof command === "string" && CONFIG_SUBCOMMAND_SET.has(command);
1740
2758
  }
2759
+ function hasVaultSubcommand(args) {
2760
+ const command = Array.isArray(args._) ? args._[0] : undefined;
2761
+ return typeof command === "string" && VAULT_SUBCOMMAND_SET.has(command);
2762
+ }
2763
+ function hasWikiSubcommand(args) {
2764
+ const command = Array.isArray(args._) ? args._[0] : undefined;
2765
+ return typeof command === "string" && WIKI_SUBCOMMAND_SET.has(command);
2766
+ }
1741
2767
  /**
1742
2768
  * Normalize argv so positional view-mode arguments after the asset ref
1743
2769
  * are rewritten into internal flags that citty can parse.
@@ -1813,7 +2839,7 @@ function loadHints(detail = "normal") {
1813
2839
  const fallback = detail === "full" ? EMBEDDED_HINTS_FULL : EMBEDDED_HINTS;
1814
2840
  // Try reading from the docs/ directory (works in dev and when installed via npm)
1815
2841
  try {
1816
- const docsPath = path.resolve(import.meta.dir ?? __dirname, `../docs/${filename}`);
2842
+ const docsPath = path.resolve(import.meta.dir ?? __dirname, `../docs/agents/${filename}`);
1817
2843
  if (fs.existsSync(docsPath)) {
1818
2844
  return fs.readFileSync(docsPath, "utf8");
1819
2845
  }
@@ -1826,21 +2852,25 @@ function loadHints(detail = "normal") {
1826
2852
  }
1827
2853
  const EMBEDDED_HINTS = `# akm CLI
1828
2854
 
1829
- You have access to a searchable library of scripts, skills, commands, agents, and knowledge documents via \`akm\`. Search your sources first before writing something from scratch.
2855
+ 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.
1830
2856
 
1831
2857
  ## Quick Reference
1832
2858
 
1833
2859
  \`\`\`sh
1834
2860
  akm search "<query>" # Search all sources
1835
2861
  akm curate "<task>" # Curate the best matches for a task
1836
- akm search "<query>" --type skill # Filter by type
2862
+ akm search "<query>" --type workflow # Filter to workflow assets
1837
2863
  akm search "<query>" --source both # Also search registries
1838
2864
  akm show <ref> # View asset details
2865
+ akm workflow next <ref> # Start or resume a workflow
1839
2866
  akm remember "Deployment needs VPN access" # Record a memory in your stash
1840
2867
  akm import ./notes/release-checklist.md # Import a knowledge doc into your stash
2868
+ akm wiki list # List available wikis
2869
+ akm wiki ingest <name> # Print the ingest workflow for a wiki
1841
2870
  akm feedback <ref> --positive|--negative # Record whether an asset helped
1842
2871
  akm add <ref> # Add a source (npm, GitHub, git, local dir)
1843
2872
  akm clone <ref> # Copy an asset to the working stash (optional --dest arg to clone to specific location)
2873
+ akm save # Commit (and push if writable remote) changes in the primary stash
1844
2874
  akm registry search "<query>" # Search all registries
1845
2875
  \`\`\`
1846
2876
 
@@ -1853,7 +2883,10 @@ akm registry search "<query>" # Search all registries
1853
2883
  | command | A prompt template with placeholders to fill in |
1854
2884
  | agent | A system prompt with model and tool hints |
1855
2885
  | knowledge | A reference doc (use \`toc\` or \`section "..."\` to navigate) |
2886
+ | workflow | Parsed steps plus workflow-specific execution commands |
1856
2887
  | memory | Recalled context (read the content for background information) |
2888
+ | vault | Key names only; use vault commands to inspect or load values safely |
2889
+ | 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. |
1857
2890
 
1858
2891
  When an asset meaningfully helps or fails, record that with \`akm feedback\` so
1859
2892
  future search ranking can learn from real usage.
@@ -1862,14 +2895,14 @@ Run \`akm -h\` for the full command reference.
1862
2895
  `;
1863
2896
  const EMBEDDED_HINTS_FULL = `# akm CLI — Full Reference
1864
2897
 
1865
- You have access to a searchable library of scripts, skills, commands, agents, and knowledge documents via \`akm\`. Search your sources first before writing something from scratch.
2898
+ 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.
1866
2899
 
1867
2900
  ## Search
1868
2901
 
1869
2902
  \`\`\`sh
1870
2903
  akm search "<query>" # Search all sources
1871
2904
  akm curate "<task>" # Curate the best matches for a task
1872
- akm search "<query>" --type skill # Filter by asset type
2905
+ akm search "<query>" --type workflow # Filter by asset type
1873
2906
  akm search "<query>" --source both # Also search registries
1874
2907
  akm search "<query>" --source registry # Search registries only
1875
2908
  akm search "<query>" --limit 10 # Limit results
@@ -1878,7 +2911,7 @@ akm search "<query>" --detail full # Include scores, paths, timing
1878
2911
 
1879
2912
  | Flag | Values | Default |
1880
2913
  | --- | --- | --- |
1881
- | \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`script\`, \`memory\`, \`any\` | \`any\` |
2914
+ | \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`workflow\`, \`script\`, \`memory\`, \`vault\`, \`wiki\`, \`any\` | \`any\` |
1882
2915
  | \`--source\` | \`stash\`, \`registry\`, \`both\` | \`stash\` |
1883
2916
  | \`--limit\` | number | \`20\` |
1884
2917
  | \`--format\` | \`json\`, \`jsonl\`, \`text\`, \`yaml\` | \`json\` |
@@ -1892,7 +2925,7 @@ Combine search + follow-up hints into a dense summary for a task or prompt.
1892
2925
  \`\`\`sh
1893
2926
  akm curate "plan a release" # Pick top matches across asset types
1894
2927
  akm curate "deploy a Bun app" --limit 3 # Keep the summary shorter
1895
- akm curate "review architecture" --type skill # Restrict to one asset type
2928
+ akm curate "review architecture" --type workflow # Restrict to one asset type
1896
2929
  \`\`\`
1897
2930
 
1898
2931
  ## Show
@@ -1904,6 +2937,7 @@ akm show script:deploy.sh # Show script (returns run command
1904
2937
  akm show skill:code-review # Show skill (returns full content)
1905
2938
  akm show command:release # Show command (returns template)
1906
2939
  akm show agent:architect # Show agent (returns system prompt)
2940
+ akm show workflow:ship-release # Show parsed workflow steps
1907
2941
  akm show knowledge:guide toc # Table of contents
1908
2942
  akm show knowledge:guide section "Auth" # Specific section
1909
2943
  akm show knowledge:guide lines 10 30 # Line range
@@ -1917,7 +2951,10 @@ akm show knowledge:my-doc # Show content (local or remote)
1917
2951
  | command | \`template\`, \`description\`, \`parameters\` |
1918
2952
  | agent | \`prompt\`, \`description\`, \`modelHint\`, \`toolPolicy\` |
1919
2953
  | knowledge | \`content\` (with view modes: \`full\`, \`toc\`, \`frontmatter\`, \`section\`, \`lines\`) |
2954
+ | workflow | \`workflowTitle\`, \`workflowParameters\`, \`steps\` |
1920
2955
  | memory | \`content\` (recalled context) |
2956
+ | vault | \`keys\`, \`comments\` |
2957
+ | wiki | \`content\` (same view modes as knowledge). For any wiki task, run \`akm wiki list\` then \`akm wiki ingest <name>\` for the workflow. |
1921
2958
 
1922
2959
  ## Capture Knowledge While You Work
1923
2960
 
@@ -1926,6 +2963,8 @@ akm remember "Deployment needs VPN access" # Record a memory in your stash
1926
2963
  akm remember --name release-retro < notes.md # Save multiline memory from stdin
1927
2964
  akm import ./docs/auth-flow.md # Import a file as knowledge
1928
2965
  akm import - --name scratch-notes < notes.md # Import stdin as a knowledge doc
2966
+ akm workflow create ship-release # Create a workflow asset in the stash
2967
+ akm workflow next workflow:ship-release # Start or resume the next workflow step
1929
2968
  akm feedback skill:code-review --positive # Record that an asset helped
1930
2969
  akm feedback agent:reviewer --negative # Record that an asset missed the mark
1931
2970
  \`\`\`
@@ -1933,22 +2972,62 @@ akm feedback agent:reviewer --negative # Record that an asset missed the
1933
2972
  Use \`akm feedback\` whenever an asset materially helps or fails so future search
1934
2973
  ranking can learn from actual usage.
1935
2974
 
1936
- ## Add & Manage Sources
2975
+ ## Wikis
2976
+
2977
+ Multi-wiki knowledge bases (Karpathy-style). Each wiki is a directory at
2978
+ \`<stashDir>/wikis/<name>/\` with \`schema.md\`, \`index.md\`, \`log.md\`, \`raw/\`,
2979
+ and agent-authored pages. akm owns lifecycle + raw-slug + lint + index
2980
+ regeneration; page edits use your native Read/Write/Edit tools.
1937
2981
 
1938
2982
  \`\`\`sh
1939
- akm add <ref> # Add a source
1940
- akm add @scope/kit # From npm (managed)
1941
- akm add owner/repo # From GitHub (managed)
1942
- akm add ./path/to/local/kit # Local directory
1943
- akm enable skills.sh # Enable the skills.sh registry
1944
- akm disable skills.sh # Disable the skills.sh registry
1945
- akm enable context-hub # Add/enable the context-hub source
1946
- akm disable context-hub # Disable the context-hub source
1947
- akm list # List all sources
1948
- akm list --kind managed # List managed sources only
1949
- akm remove <target> # Remove by id, ref, path, or name
1950
- akm update --all # Update all managed sources
1951
- akm update <target> --force # Force re-download
2983
+ akm wiki list # List wikis (name, pages, raws, last-modified)
2984
+ akm wiki create research # Scaffold a new wiki
2985
+ akm wiki show research # Path, description, counts, last 3 log entries
2986
+ akm wiki pages research # Page refs + descriptions (excludes schema/index/log/raw)
2987
+ akm wiki search research "attention" # Scoped search (equivalent to --type wiki --wiki research)
2988
+ akm wiki stash research ./paper.md # Copy source into raw/<slug>.md (never overwrites)
2989
+ echo "..." | akm wiki stash research - # stdin form
2990
+ akm wiki lint research # Structural checks: orphans, broken xrefs, uncited raws, stale index
2991
+ akm wiki ingest research # Print the ingest workflow for this wiki (no action)
2992
+ akm wiki remove research --force # Delete pages/schema/index/log; preserves raw/
2993
+ akm wiki remove research --force --with-sources # Full nuke, including raw/
2994
+ \`\`\`
2995
+
2996
+ **For any wiki task, start with \`akm wiki list\`, then \`akm wiki ingest <name>\`
2997
+ to get the step-by-step workflow.** Wiki pages are also addressable as
2998
+ \`wiki:<name>/<page-path>\` and show up in stash-wide \`akm search\` as
2999
+ \`type: wiki\`. Files under \`raw/\` and the wiki root infrastructure files
3000
+ \`schema.md\`, \`index.md\`, and \`log.md\` are not indexed and do not appear in
3001
+ search results. No \`--llm\` anywhere — akm never reasons about page content.
3002
+
3003
+ ## Vaults
3004
+
3005
+ Encrypted-at-rest key/value stores for secrets. Each vault is a \`.env\`-format
3006
+ file at \`<stashDir>/vaults/<name>.env\`.
3007
+
3008
+ \`\`\`sh
3009
+ akm vault create prod # Create a new vault
3010
+ akm vault set prod DB_URL postgres://... # Set a key (or KEY=VALUE combined form)
3011
+ akm vault set prod DB_URL=postgres://... # Combined KEY=VALUE form also works
3012
+ akm vault unset prod DB_URL # Remove a key
3013
+ akm vault list vault:prod # List key names (no values)
3014
+ akm vault show vault:prod # Same as list (alias)
3015
+ akm vault load vault:prod # Print export statements to source
3016
+ \`\`\`
3017
+
3018
+ ## Workflows
3019
+
3020
+ Step-based workflows stored as \`<stashDir>/workflows/<name>.md\`.
3021
+
3022
+ \`\`\`sh
3023
+ akm workflow template # Print a starter workflow template
3024
+ akm workflow create ship-release # Scaffold a new workflow asset
3025
+ akm workflow start workflow:ship-release # Start a new run
3026
+ akm workflow next workflow:ship-release # Advance to the next step (or auto-start)
3027
+ akm workflow complete <run-id> # Mark a step complete and advance
3028
+ akm workflow status <run-id> # Show current run status
3029
+ akm workflow resume <run-id> # Resume a blocked or failed run
3030
+ akm workflow list # List all workflow runs
1952
3031
  \`\`\`
1953
3032
 
1954
3033
  ## Clone
@@ -1965,6 +3044,47 @@ akm clone "npm:@scope/pkg//script:deploy.sh" # Clone from remote package
1965
3044
 
1966
3045
  When \`--dest\` is provided, \`akm init\` is not required first.
1967
3046
 
3047
+ ## Save
3048
+
3049
+ Commit local changes in a git-backed stash. Behaviour adapts automatically:
3050
+
3051
+ - **Not a git repo** — no-op (silent skip)
3052
+ - **Git repo, no remote** — stage and commit only (the default stash always falls here)
3053
+ - **Git repo, has remote, not writable** — stage and commit only
3054
+ - **Git repo, has remote, \`writable: true\`** — stage, commit, and push
3055
+
3056
+ \`\`\`sh
3057
+ akm save # Save primary stash (timestamp message)
3058
+ akm save -m "Add deploy skill" # Save with explicit message
3059
+ akm save my-skills # Save a named writable git stash
3060
+ akm save my-skills -m "Update patterns" # Save named stash with message
3061
+ \`\`\`
3062
+
3063
+ The \`--writable\` flag on \`akm add\` opts a remote git stash into push-on-save:
3064
+
3065
+ \`\`\`sh
3066
+ akm add git@github.com:org/skills.git --provider git --name my-skills --writable
3067
+ \`\`\`
3068
+
3069
+ ## Add & Manage Sources
3070
+
3071
+ \`\`\`sh
3072
+ akm add <ref> # Add a source
3073
+ akm add @scope/kit # From npm (managed)
3074
+ akm add owner/repo # From GitHub (managed)
3075
+ akm add ./path/to/local/kit # Local directory
3076
+ akm add git@github.com:org/repo.git --provider git --name my-skills --writable
3077
+ akm enable skills.sh # Enable the skills.sh registry
3078
+ akm disable skills.sh # Disable the skills.sh registry
3079
+ akm enable context-hub # Add/enable the context-hub source
3080
+ akm disable context-hub # Disable the context-hub source
3081
+ akm list # List all sources
3082
+ akm list --kind managed # List managed sources only
3083
+ akm remove <target> # Remove by id, ref, path, or name
3084
+ akm update --all # Update all managed sources
3085
+ akm update <target> --force # Force re-download
3086
+ \`\`\`
3087
+
1968
3088
  ## Registries
1969
3089
 
1970
3090
  \`\`\`sh
@@ -1996,8 +3116,9 @@ akm init # Initialize working stash
1996
3116
  akm index # Rebuild search index
1997
3117
  akm index --full # Full reindex
1998
3118
  akm list # List all sources
1999
- akm upgrade # Upgrade akm binary
3119
+ akm upgrade # Upgrade akm using its install method
2000
3120
  akm upgrade --check # Check for updates
3121
+ akm help migrate 0.5.0 # Print migration notes for a release
2001
3122
  akm hints # Print this reference
2002
3123
  akm completions # Print bash completion script
2003
3124
  akm completions --install # Install completions