akm-cli 0.1.3 → 0.2.0

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.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Central registry for asset type renderer and action builder maps.
3
+ *
4
+ * Previously these maps lived in `local-search.ts` and were wired into
5
+ * `asset-spec.ts` via a fragile `_setAssetTypeHooks` deferred callback
6
+ * pattern. If `local-search.ts` was imported after `registerAssetType()`
7
+ * calls, hooks would be silently dropped.
8
+ *
9
+ * This module is a simple singleton that both `asset-spec.ts` and
10
+ * `local-search.ts` import from, eliminating the import-order dependency
11
+ * entirely.
12
+ */
13
+ /** Map asset types to their primary renderer names. */
14
+ export const TYPE_TO_RENDERER = {
15
+ script: "script-source",
16
+ skill: "skill-md",
17
+ command: "command-md",
18
+ agent: "agent-md",
19
+ knowledge: "knowledge-md",
20
+ memory: "memory-md",
21
+ };
22
+ /** Map asset types to action builder functions for search results. */
23
+ export const ACTION_BUILDERS = {
24
+ script: (ref) => `akm show ${ref} -> execute the run command`,
25
+ skill: (ref) => `akm show ${ref} -> follow the instructions`,
26
+ command: (ref) => `akm show ${ref} -> fill placeholders and dispatch`,
27
+ agent: (ref) => `akm show ${ref} -> dispatch with full prompt`,
28
+ knowledge: (ref) => `akm show ${ref} -> read reference material`,
29
+ memory: (ref) => `akm show ${ref} -> recall context`,
30
+ };
31
+ /**
32
+ * Register a type-to-renderer mapping.
33
+ *
34
+ * Called by `registerAssetType()` in `asset-spec.ts` when a spec includes
35
+ * `rendererName`, or directly by extension code.
36
+ */
37
+ export function registerTypeRenderer(type, rendererName) {
38
+ TYPE_TO_RENDERER[type] = rendererName;
39
+ }
40
+ /**
41
+ * Register an action builder for an asset type.
42
+ *
43
+ * Called by `registerAssetType()` in `asset-spec.ts` when a spec includes
44
+ * `actionBuilder`, or directly by extension code.
45
+ */
46
+ export function registerActionBuilder(type, builder) {
47
+ ACTION_BUILDERS[type] = builder;
48
+ }
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { registerActionBuilder, registerTypeRenderer } from "./asset-registry";
2
3
  import { toPosix } from "./common";
3
4
  const markdownSpec = {
4
5
  isRelevantFile: (fileName) => path.extname(fileName).toLowerCase() === ".md",
@@ -56,28 +57,6 @@ const ASSET_SPECS_INTERNAL = {
56
57
  memory: { stashDir: "memories", ...markdownSpec },
57
58
  };
58
59
  export const ASSET_SPECS = ASSET_SPECS_INTERNAL;
59
- /**
60
- * Deferred hooks set by `local-search.ts` at module init time to avoid a
61
- * circular dependency (asset-spec → local-search → asset-spec).
62
- *
63
- * When `registerAssetType` is called with a spec that includes `rendererName`
64
- * or `actionBuilder`, these hooks are invoked automatically so callers only
65
- * need a single `registerAssetType(type, spec)` call to fully register a new
66
- * asset type — no separate `registerTypeRenderer`/`registerActionBuilder` calls
67
- * are required.
68
- */
69
- let _registerTypeRenderer;
70
- let _registerActionBuilder;
71
- /**
72
- * Called once by `local-search.ts` during module initialization to wire in the
73
- * renderer and action-builder registration hooks.
74
- *
75
- * @internal — not part of the public extension API; use `registerAssetType` instead.
76
- */
77
- export function _setAssetTypeHooks(rendererHook, actionBuilderHook) {
78
- _registerTypeRenderer = rendererHook;
79
- _registerActionBuilder = actionBuilderHook;
80
- }
81
60
  /**
82
61
  * Register a custom asset type with the akm asset system.
83
62
  *
@@ -109,27 +88,27 @@ export function _setAssetTypeHooks(rendererHook, actionBuilderHook) {
109
88
  * });
110
89
  * ```
111
90
  *
112
- * If `rendererName` or `actionBuilder` is provided but the hooks have not yet
113
- * been wired (i.e. `local-search.ts` has not been imported), the values are
114
- * stored in the spec and will take effect once the hooks are set.
91
+ * Renderer and action builder registration is handled directly via the
92
+ * `asset-registry` singleton no deferred hooks or import-order concerns.
115
93
  */
116
94
  export function registerAssetType(type, spec) {
117
95
  ASSET_SPECS_INTERNAL[type] = spec;
118
96
  TYPE_DIRS[type] = spec.stashDir;
119
- ASSET_TYPES = getAssetTypes();
97
+ ASSET_TYPES.length = 0;
98
+ ASSET_TYPES.push(...getAssetTypes());
120
99
  // Auto-register renderer and action builder if provided in spec
121
- if (spec.rendererName && _registerTypeRenderer) {
122
- _registerTypeRenderer(type, spec.rendererName);
100
+ if (spec.rendererName) {
101
+ registerTypeRenderer(type, spec.rendererName);
123
102
  }
124
- if (spec.actionBuilder && _registerActionBuilder) {
125
- _registerActionBuilder(type, spec.actionBuilder);
103
+ if (spec.actionBuilder) {
104
+ registerActionBuilder(type, spec.actionBuilder);
126
105
  }
127
106
  }
128
107
  export function getAssetTypes() {
129
108
  return Object.keys(ASSET_SPECS_INTERNAL);
130
109
  }
131
- /** Warning: mutable `let` — stale if captured before `registerAssetType()` calls. Prefer `getAssetTypes()`. */
132
- export let ASSET_TYPES = getAssetTypes();
110
+ /** Warning: mutable array — stale if captured before `registerAssetType()` calls. Prefer `getAssetTypes()`. */
111
+ export const ASSET_TYPES = getAssetTypes();
133
112
  export const TYPE_DIRS = Object.fromEntries(Object.entries(ASSET_SPECS_INTERNAL).map(([type, spec]) => [type, spec.stashDir]));
134
113
  export function isRelevantAssetFile(assetType, fileName) {
135
114
  return ASSET_SPECS[assetType]?.isRelevantFile(fileName) ?? false;
package/dist/cli.js CHANGED
@@ -6,8 +6,10 @@ import { resolveStashDir } from "./common";
6
6
  import { generateBashCompletions, installBashCompletions } from "./completions";
7
7
  import { DEFAULT_CONFIG, getConfigPath, loadConfig, saveConfig } from "./config";
8
8
  import { getConfigValue, listConfig, setConfigValue, unsetConfigValue } from "./config-cli";
9
+ import { closeDatabase, openDatabase } from "./db";
9
10
  import { ConfigError, NotFoundError, UsageError } from "./errors";
10
11
  import { akmIndex } from "./indexer";
12
+ import { assembleInfo } from "./info";
11
13
  import { akmInit } from "./init";
12
14
  import { akmList, akmRemove, akmUpdate } from "./installed-kits";
13
15
  import { getCacheDir, getDbPath, getDefaultStashDir } from "./paths";
@@ -19,42 +21,15 @@ import { akmClone } from "./stash-clone";
19
21
  import { akmSearch, parseSearchSource } from "./stash-search";
20
22
  import { akmShowUnified } from "./stash-show";
21
23
  import { addStash, listStashes, removeStash } from "./stash-source-manage";
24
+ import { insertUsageEvent } from "./usage-events";
25
+ import { pkgVersion } from "./version";
22
26
  import { setQuiet, warn } from "./warn";
23
- // Version: prefer compile-time define, then package.json, then fallback
24
- const pkgVersion = (() => {
25
- // Injected at compile time via `bun build --define`
26
- if (typeof AKM_VERSION !== "undefined")
27
- return AKM_VERSION;
28
- try {
29
- const pkgPath = path.resolve(import.meta.dir ?? __dirname, "../package.json");
30
- if (fs.existsSync(pkgPath)) {
31
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
32
- if (typeof pkg.version === "string")
33
- return pkg.version;
34
- }
35
- }
36
- catch {
37
- // swallow — running as compiled binary without package.json
38
- }
39
- return "0.0.0-dev";
40
- })();
41
- const OUTPUT_FORMATS = ["json", "yaml", "text"];
42
- const DETAIL_LEVELS = ["brief", "normal", "full"];
27
+ const OUTPUT_FORMATS = ["json", "yaml", "text", "jsonl"];
28
+ const DETAIL_LEVELS = ["brief", "normal", "full", "summary"];
43
29
  const NORMAL_DESCRIPTION_LIMIT = 250;
44
30
  const CONTEXT_HUB_ALIAS_REF = "context-hub";
45
31
  const CONTEXT_HUB_ALIAS_URL = "https://github.com/andrewyng/context-hub";
46
- function hasBunYAML(b) {
47
- // biome-ignore lint/suspicious/noExplicitAny: type guard for runtime feature detection
48
- return typeof b.YAML?.stringify === "function";
49
- }
50
- /** Try Bun.YAML.stringify; fall back to JSON if the API is unavailable */
51
- function yamlStringify(obj) {
52
- if (hasBunYAML(Bun)) {
53
- return Bun.YAML.stringify(obj);
54
- }
55
- warn("YAML output not available, using JSON");
56
- return JSON.stringify(obj, null, 2);
57
- }
32
+ import { stringify as yamlStringify } from "yaml";
58
33
  function parseOutputFormat(value) {
59
34
  if (!value)
60
35
  return undefined;
@@ -79,15 +54,25 @@ function parseFlagValue(flag) {
79
54
  }
80
55
  return undefined;
81
56
  }
57
+ // Uses process.argv directly because the global output() function (called by all
58
+ // commands) needs this flag but doesn't have access to citty's parsed args.
59
+ function hasBooleanFlag(flag) {
60
+ return process.argv.some((arg) => arg === flag || arg === `${flag}=true`);
61
+ }
82
62
  function resolveOutputMode() {
83
63
  const config = loadConfig();
84
64
  const format = parseOutputFormat(parseFlagValue("--format")) ?? config.output?.format ?? "json";
85
65
  const detail = parseDetailLevel(parseFlagValue("--detail")) ?? config.output?.detail ?? "brief";
86
- return { format, detail };
66
+ const forAgent = hasBooleanFlag("--for-agent");
67
+ return { format, detail, forAgent };
87
68
  }
88
69
  function output(command, result) {
89
70
  const mode = resolveOutputMode();
90
- const shaped = shapeForCommand(command, result, mode.detail);
71
+ const shaped = shapeForCommand(command, result, mode.detail, mode.forAgent);
72
+ if (mode.format === "jsonl") {
73
+ outputJsonl(command, shaped);
74
+ return;
75
+ }
91
76
  switch (mode.format) {
92
77
  case "json":
93
78
  console.log(JSON.stringify(shaped, null, 2));
@@ -102,27 +87,57 @@ function output(command, result) {
102
87
  }
103
88
  }
104
89
  }
105
- function shapeForCommand(command, result, detail) {
90
+ function outputJsonl(command, shaped) {
91
+ if (command === "search" || command === "registry-search") {
92
+ const r = shaped;
93
+ const hits = Array.isArray(r.hits) ? r.hits : [];
94
+ for (const hit of hits) {
95
+ console.log(JSON.stringify(hit));
96
+ }
97
+ const registryHits = Array.isArray(r.registryHits) ? r.registryHits : [];
98
+ for (const hit of registryHits) {
99
+ console.log(JSON.stringify(hit));
100
+ }
101
+ return;
102
+ }
103
+ // For non-search commands, output the whole object as a single JSONL line
104
+ console.log(JSON.stringify(shaped));
105
+ }
106
+ function shapeForCommand(command, result, detail, forAgent = false) {
106
107
  switch (command) {
107
108
  case "search":
108
- return shapeSearchOutput(result, detail);
109
+ return shapeSearchOutput(result, detail, forAgent);
109
110
  case "registry-search":
110
111
  return shapeRegistrySearchOutput(result, detail);
111
112
  case "show":
112
- return shapeShowOutput(result, detail);
113
+ return shapeShowOutput(result, detail, forAgent);
113
114
  default:
114
115
  return result;
115
116
  }
116
117
  }
117
- function shapeSearchOutput(result, detail) {
118
+ function shapeSearchOutput(result, detail, forAgent = false) {
118
119
  const hits = Array.isArray(result.hits) ? result.hits : [];
119
- const shapedHits = hits.map((hit) => shapeSearchHit(hit, detail));
120
+ const registryHits = Array.isArray(result.registryHits) ? result.registryHits : [];
121
+ const shapedHits = forAgent
122
+ ? hits.map((hit) => shapeSearchHitForAgent(hit))
123
+ : hits.map((hit) => shapeSearchHit(hit, detail));
124
+ const shapedRegistryHits = forAgent
125
+ ? registryHits.map((hit) => shapeSearchHitForAgent(hit))
126
+ : registryHits.map((hit) => shapeSearchHit(hit, detail));
127
+ if (forAgent) {
128
+ return {
129
+ hits: shapedHits,
130
+ ...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}),
131
+ ...(result.tip ? { tip: result.tip } : {}),
132
+ };
133
+ }
120
134
  if (detail === "full") {
121
135
  return {
122
136
  schemaVersion: result.schemaVersion,
123
137
  stashDir: result.stashDir,
124
138
  source: result.source,
125
139
  hits: shapedHits,
140
+ ...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}),
126
141
  ...(result.tip ? { tip: result.tip } : {}),
127
142
  ...(result.warnings ? { warnings: result.warnings } : {}),
128
143
  ...(result.timing ? { timing: result.timing } : {}),
@@ -130,6 +145,7 @@ function shapeSearchOutput(result, detail) {
130
145
  }
131
146
  return {
132
147
  hits: shapedHits,
148
+ ...(shapedRegistryHits.length > 0 ? { registryHits: shapedRegistryHits } : {}),
133
149
  ...(result.tip ? { tip: result.tip } : {}),
134
150
  ...(Array.isArray(result.warnings) && result.warnings.length > 0 ? { warnings: result.warnings } : {}),
135
151
  };
@@ -153,9 +169,9 @@ function shapeRegistrySearchOutput(result, detail) {
153
169
  }
154
170
  function shapeAssetHit(hit, detail) {
155
171
  if (detail === "brief")
156
- return pickFields(hit, ["assetName", "assetType", "action"]);
172
+ return pickFields(hit, ["assetName", "assetType", "action", "estimatedTokens"]);
157
173
  if (detail === "normal") {
158
- return capDescription(pickFields(hit, ["assetName", "assetType", "description", "kit", "action"]), NORMAL_DESCRIPTION_LIMIT);
174
+ return capDescription(pickFields(hit, ["assetName", "assetType", "description", "kit", "action", "estimatedTokens"]), NORMAL_DESCRIPTION_LIMIT);
159
175
  }
160
176
  return hit;
161
177
  }
@@ -170,12 +186,17 @@ function shapeSearchHit(hit, detail) {
170
186
  }
171
187
  // Stash hit (local or remote)
172
188
  if (detail === "brief")
173
- return pickFields(hit, ["type", "name", "action"]);
189
+ return pickFields(hit, ["type", "name", "action", "estimatedTokens"]);
174
190
  if (detail === "normal") {
175
- return capDescription(pickFields(hit, ["type", "name", "description", "action", "score"]), NORMAL_DESCRIPTION_LIMIT);
191
+ return capDescription(pickFields(hit, ["type", "name", "description", "action", "score", "estimatedTokens"]), NORMAL_DESCRIPTION_LIMIT);
176
192
  }
177
193
  return hit;
178
194
  }
195
+ /** Agent-optimized search hit: only fields an LLM agent needs to decide and act */
196
+ function shapeSearchHitForAgent(hit) {
197
+ const picked = pickFields(hit, ["name", "ref", "type", "description", "action", "score", "estimatedTokens"]);
198
+ return capDescription(picked, NORMAL_DESCRIPTION_LIMIT);
199
+ }
179
200
  function capDescription(hit, limit) {
180
201
  if (typeof hit.description !== "string")
181
202
  return hit;
@@ -190,13 +211,35 @@ function truncateDescription(description, limit) {
190
211
  const safe = lastSpace >= Math.floor(limit * 0.6) ? truncated.slice(0, lastSpace) : truncated;
191
212
  return `${safe.trimEnd()}...`;
192
213
  }
193
- function shapeShowOutput(result, detail) {
214
+ function shapeShowOutput(result, detail, forAgent = false) {
215
+ if (forAgent) {
216
+ return pickFields(result, [
217
+ "type",
218
+ "name",
219
+ "description",
220
+ "action",
221
+ "content",
222
+ "template",
223
+ "prompt",
224
+ "run",
225
+ "setup",
226
+ "cwd",
227
+ "toolPolicy",
228
+ "modelHint",
229
+ "agent",
230
+ "parameters",
231
+ ]);
232
+ }
233
+ if (detail === "summary") {
234
+ return pickFields(result, ["type", "name", "description", "tags", "parameters", "action", "run", "origin"]);
235
+ }
194
236
  const base = pickFields(result, [
195
237
  "type",
196
238
  "name",
197
239
  "origin",
198
240
  "action",
199
241
  "description",
242
+ "tags",
200
243
  "content",
201
244
  "template",
202
245
  "prompt",
@@ -342,11 +385,13 @@ function formatPlain(command, result, detail) {
342
385
  }
343
386
  function formatSearchPlain(r, detail) {
344
387
  const hits = r.hits ?? [];
345
- if (hits.length === 0) {
388
+ const registryHits = r.registryHits ?? [];
389
+ const allHits = [...hits, ...registryHits];
390
+ if (allHits.length === 0) {
346
391
  return r.tip ? String(r.tip) : "No results found.";
347
392
  }
348
393
  const lines = [];
349
- for (const hit of hits) {
394
+ for (const hit of allHits) {
350
395
  const type = hit.type ?? "unknown";
351
396
  const name = hit.name ?? "unnamed";
352
397
  const score = hit.score != null ? ` (score: ${hit.score})` : "";
@@ -444,6 +489,15 @@ const indexCommand = defineCommand({
444
489
  });
445
490
  },
446
491
  });
492
+ const infoCommand = defineCommand({
493
+ meta: { name: "info", description: "Show system capabilities, configuration, and index stats as JSON" },
494
+ run() {
495
+ return runWithJsonErrors(() => {
496
+ const result = assembleInfo();
497
+ output("info", result);
498
+ });
499
+ },
500
+ });
447
501
  const searchCommand = defineCommand({
448
502
  meta: { name: "search", description: "Search the stash" },
449
503
  args: {
@@ -454,8 +508,8 @@ const searchCommand = defineCommand({
454
508
  },
455
509
  limit: { type: "string", description: "Maximum number of results" },
456
510
  source: { type: "string", description: "Search source (stash|registry|both)", default: "stash" },
457
- format: { type: "string", description: "Output format (json|text|yaml)" },
458
- detail: { type: "string", description: "Detail level (brief|normal|full)" },
511
+ format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
512
+ detail: { type: "string", description: "Detail level (brief|normal|full|summary)" },
459
513
  },
460
514
  async run({ args }) {
461
515
  await runWithJsonErrors(async () => {
@@ -586,8 +640,8 @@ const showCommand = defineCommand({
586
640
  },
587
641
  args: {
588
642
  ref: { type: "positional", description: "Asset ref (type:name)", required: true },
589
- format: { type: "string", description: "Output format (json|text|yaml)" },
590
- detail: { type: "string", description: "Detail level (brief|normal|full)" },
643
+ format: { type: "string", description: "Output format (json|jsonl|text|yaml)" },
644
+ detail: { type: "string", description: "Detail level (brief|normal|full|summary)" },
591
645
  akmView: { type: "string", description: "Internal positional knowledge view mode parser" },
592
646
  akmHeading: { type: "string", description: "Internal positional section heading parser" },
593
647
  akmStart: { type: "string", description: "Internal positional start-line parser" },
@@ -617,7 +671,10 @@ const showCommand = defineCommand({
617
671
  throw new UsageError(`Unknown view mode: ${args.akmView}. Expected one of: full|toc|frontmatter|section|lines`);
618
672
  }
619
673
  }
620
- const result = await akmShowUnified({ ref: args.ref, view });
674
+ // Map CLI detail level to ShowDetailLevel for the show function
675
+ const cliDetail = resolveOutputMode().detail;
676
+ const showDetail = cliDetail === "summary" ? "summary" : undefined;
677
+ const result = await akmShowUnified({ ref: args.ref, view, detail: showDetail });
621
678
  output("show", result);
622
679
  });
623
680
  },
@@ -932,6 +989,47 @@ const stashCommand = defineCommand({
932
989
  meta: { name: "stash", description: "Manage additional stashes (local directories and remote providers)" },
933
990
  subCommands: buildSourceSubCommands("stash"),
934
991
  });
992
+ const feedbackCommand = defineCommand({
993
+ meta: {
994
+ name: "feedback",
995
+ description: "Record positive or negative feedback for a stash asset",
996
+ },
997
+ args: {
998
+ ref: { type: "positional", description: "Asset ref (type:name)", required: true },
999
+ positive: { type: "boolean", description: "Record positive feedback", default: false },
1000
+ negative: { type: "boolean", description: "Record negative feedback", default: false },
1001
+ note: { type: "string", description: "Optional note to attach to the feedback" },
1002
+ },
1003
+ run({ args }) {
1004
+ return runWithJsonErrors(() => {
1005
+ const ref = args.ref.trim();
1006
+ if (!ref) {
1007
+ throw new UsageError("Asset ref is required. Usage: akm feedback <ref> --positive|--negative");
1008
+ }
1009
+ if (args.positive && args.negative) {
1010
+ throw new UsageError("Specify either --positive or --negative, not both.");
1011
+ }
1012
+ if (!args.positive && !args.negative) {
1013
+ throw new UsageError("Specify --positive or --negative.");
1014
+ }
1015
+ const signal = args.positive ? "positive" : "negative";
1016
+ const metadata = args.note ? JSON.stringify({ note: args.note }) : undefined;
1017
+ const db = openDatabase();
1018
+ try {
1019
+ insertUsageEvent(db, {
1020
+ event_type: "feedback",
1021
+ entry_ref: ref,
1022
+ signal,
1023
+ metadata,
1024
+ });
1025
+ }
1026
+ finally {
1027
+ closeDatabase(db);
1028
+ }
1029
+ output("feedback", { ok: true, ref, signal, note: args.note ?? null });
1030
+ });
1031
+ },
1032
+ });
935
1033
  const hintsCommand = defineCommand({
936
1034
  meta: {
937
1035
  name: "hints",
@@ -992,6 +1090,7 @@ const main = defineCommand({
992
1090
  setup: setupCommand,
993
1091
  init: initCommand,
994
1092
  index: indexCommand,
1093
+ info: infoCommand,
995
1094
  add: addCommand,
996
1095
  list: listCommand,
997
1096
  remove: removeCommand,
@@ -1004,6 +1103,7 @@ const main = defineCommand({
1004
1103
  stash: stashCommand,
1005
1104
  registry: registryCommand,
1006
1105
  config: configCommand,
1106
+ feedback: feedbackCommand,
1007
1107
  hints: hintsCommand,
1008
1108
  completions: completionsCommand,
1009
1109
  },
@@ -1057,9 +1157,9 @@ function buildHint(message) {
1057
1157
  if (message.includes("Invalid value for --source"))
1058
1158
  return "Pick one of: stash, registry, both.";
1059
1159
  if (message.includes("Invalid value for --format"))
1060
- return "Pick one of: json, text, yaml.";
1160
+ return "Pick one of: json, jsonl, text, yaml.";
1061
1161
  if (message.includes("Invalid value for --detail"))
1062
- return "Pick one of: brief, normal, full.";
1162
+ return "Pick one of: brief, normal, full, summary.";
1063
1163
  if (message.includes("expected JSON object with endpoint and model")) {
1064
1164
  return 'Quote JSON values in your shell, for example: akm config set embedding \'{"endpoint":"http://localhost:11434/v1/embeddings","model":"nomic-embed-text"}\'.';
1065
1165
  }
@@ -1095,7 +1195,7 @@ function normalizeShowArgv(argv) {
1095
1195
  const showArgs = [];
1096
1196
  for (let i = 0; i < rest.length; i++) {
1097
1197
  const arg = rest[i];
1098
- if (arg === "--quiet" || arg === "-q") {
1198
+ if (arg === "--quiet" || arg === "-q" || arg === "--for-agent" || arg === "--for-agent=true") {
1099
1199
  globalFlags.push(arg);
1100
1200
  continue;
1101
1201
  }
@@ -1204,8 +1304,9 @@ akm search "<query>" --detail full # Include scores, paths, timing
1204
1304
  | \`--type\` | \`skill\`, \`command\`, \`agent\`, \`knowledge\`, \`script\`, \`memory\`, \`any\` | \`any\` |
1205
1305
  | \`--source\` | \`stash\`, \`registry\`, \`both\` | \`stash\` |
1206
1306
  | \`--limit\` | number | \`20\` |
1207
- | \`--format\` | \`json\`, \`text\`, \`yaml\` | \`json\` |
1208
- | \`--detail\` | \`brief\`, \`normal\`, \`full\` | \`brief\` |
1307
+ | \`--format\` | \`json\`, \`jsonl\`, \`text\`, \`yaml\` | \`json\` |
1308
+ | \`--detail\` | \`brief\`, \`normal\`, \`full\`, \`summary\` | \`brief\` |
1309
+ | \`--for-agent\` | boolean | \`false\` |
1209
1310
 
1210
1311
  ## Show
1211
1312
 
@@ -1219,7 +1320,7 @@ akm show agent:architect # Show agent (returns system promp
1219
1320
  akm show knowledge:guide toc # Table of contents
1220
1321
  akm show knowledge:guide section "Auth" # Specific section
1221
1322
  akm show knowledge:guide lines 10 30 # Line range
1222
- akm show viking://resources/my-doc # Show remote OpenViking content
1323
+ akm show knowledge:my-doc # Show content (local or remote)
1223
1324
  \`\`\`
1224
1325
 
1225
1326
  | Type | Key fields returned |
@@ -1307,11 +1408,14 @@ akm completions --install # Install completions
1307
1408
  All commands accept \`--format\` and \`--detail\` flags:
1308
1409
 
1309
1410
  - \`--format json\` (default) — structured JSON
1411
+ - \`--format jsonl\` — one JSON object per line (streaming-friendly)
1310
1412
  - \`--format text\` — human-readable plain text
1311
1413
  - \`--format yaml\` — YAML output
1312
1414
  - \`--detail brief\` (default) — compact output
1313
1415
  - \`--detail normal\` — adds tags, refs, origins
1314
1416
  - \`--detail full\` — includes scores, paths, timing, debug info
1417
+ - \`--detail summary\` — metadata only (no content/template/prompt), under 200 tokens
1418
+ - \`--for-agent\` — agent-optimized output: strips non-actionable fields (takes precedence over \`--detail\`)
1315
1419
 
1316
1420
  Run \`akm -h\` or \`akm <command> -h\` for per-command help.
1317
1421
  `;
@@ -1,11 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { getAssetTypes } from "./asset-spec";
4
5
  // ── Known flag values ────────────────────────────────────────────────────────
5
6
  const FLAG_VALUES = {
6
7
  "--format": ["json", "text", "yaml"],
7
8
  "--detail": ["brief", "normal", "full"],
8
- "--type": ["skill", "command", "agent", "knowledge", "script", "memory", "any"],
9
+ "--type": () => [...getAssetTypes(), "any"],
9
10
  "--source": ["stash", "registry", "both"],
10
11
  "--shell": ["bash"],
11
12
  };
@@ -57,7 +58,8 @@ export function generateBashCompletions(cmd) {
57
58
  }
58
59
  // Build flag-value completion cases
59
60
  const valueCases = [];
60
- for (const [flag, values] of Object.entries(FLAG_VALUES)) {
61
+ for (const [flag, valuesOrFn] of Object.entries(FLAG_VALUES)) {
62
+ const values = typeof valuesOrFn === "function" ? valuesOrFn() : valuesOrFn;
61
63
  valueCases.push(` ${flag})
62
64
  COMPREPLY=( $(compgen -W "${values.join(" ")}" -- "\${cur}") )
63
65
  return 0
package/dist/config.js CHANGED
@@ -22,6 +22,9 @@ export function getConfigPath() {
22
22
  }
23
23
  // ── Load / Save / Update ────────────────────────────────────────────────────
24
24
  let cachedConfig;
25
+ export function resetConfigCache() {
26
+ cachedConfig = undefined;
27
+ }
25
28
  export function loadConfig() {
26
29
  const configPath = getConfigPath();
27
30
  let stat;
@@ -184,10 +187,11 @@ const URL_FIELD_NAMES = new Set(["url", "endpoint", "artifactUrl"]);
184
187
  */
185
188
  function expandEnvVars(value, fieldName) {
186
189
  if (typeof value === "string") {
187
- // Skip URL-type fields by name or by value prefix
188
- if ((fieldName !== undefined && URL_FIELD_NAMES.has(fieldName)) ||
189
- value.startsWith("http://") ||
190
- value.startsWith("https://")) {
190
+ // Skip URL-type fields by name or by value prefix, unless they contain ${VAR} syntax
191
+ if (!value.includes("${") &&
192
+ ((fieldName !== undefined && URL_FIELD_NAMES.has(fieldName)) ||
193
+ value.startsWith("http://") ||
194
+ value.startsWith("https://"))) {
191
195
  return value;
192
196
  }
193
197
  return value.replace(/\$\{([^}]+)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, braced, bare) => {
@@ -274,14 +278,35 @@ function parseEmbeddingConfig(value) {
274
278
  if (typeof value !== "object" || value === null || Array.isArray(value))
275
279
  return undefined;
276
280
  const obj = value;
277
- if (typeof obj.endpoint !== "string" || !obj.endpoint)
281
+ // Extract localModel early it's valid even without a remote endpoint
282
+ const localModel = typeof obj.localModel === "string" && obj.localModel ? obj.localModel : undefined;
283
+ // If no endpoint is provided, the config is only valid when localModel is set
284
+ // (local-only embedding configuration).
285
+ // Sentinel: { endpoint: "", model: "" } means "local-only" — use hasRemoteEndpoint()
286
+ // (in embedder.ts) to distinguish from a real remote config. Do NOT check
287
+ // endpoint/model directly in consuming code.
288
+ if (typeof obj.endpoint !== "string" || !obj.endpoint) {
289
+ if (localModel) {
290
+ return { endpoint: "", model: "", localModel };
291
+ }
278
292
  return undefined;
293
+ }
279
294
  if (!obj.endpoint.startsWith("http://") && !obj.endpoint.startsWith("https://")) {
280
295
  console.warn(`[akm] Ignoring embedding config: endpoint must start with http:// or https://, got "${obj.endpoint}"`);
296
+ // Still return localModel-only config if localModel was set
297
+ if (localModel) {
298
+ return { endpoint: "", model: "", localModel };
299
+ }
281
300
  return undefined;
282
301
  }
283
- if (typeof obj.model !== "string" || !obj.model)
302
+ if (typeof obj.model !== "string" || !obj.model) {
303
+ // No remote model, but localModel may still be valid
304
+ if (localModel) {
305
+ console.warn(`[akm] Embedding endpoint "${obj.endpoint}" ignored: model is required for remote embeddings. Using local model only.`);
306
+ return { endpoint: "", model: "", localModel };
307
+ }
284
308
  return undefined;
309
+ }
285
310
  const result = {
286
311
  endpoint: obj.endpoint,
287
312
  model: obj.model,
@@ -301,6 +326,9 @@ function parseEmbeddingConfig(value) {
301
326
  if (typeof obj.apiKey === "string" && obj.apiKey) {
302
327
  result.apiKey = obj.apiKey;
303
328
  }
329
+ if (localModel) {
330
+ result.localModel = localModel;
331
+ }
304
332
  return result;
305
333
  }
306
334
  function parseLlmConfig(value) {