@odla-ai/cli 0.21.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,6 +37,26 @@ npx @odla-ai/cli runbook list # what exists
37
37
  Reading works for any signed-in developer and needs no `odla.config.mjs` — the
38
38
  advice is most useful before there is a project.
39
39
 
40
+ Runbooks tell an agent how to operate; PM tells every agent what this project is
41
+ trying to prove, what is active, what was decided, and what is broken. Once the
42
+ app is registered, start each session by reading its doing tasks, open bugs and
43
+ goals, and recent decisions:
44
+
45
+ ```bash
46
+ npx odla-ai pm task list --app <appId> --column doing
47
+ npx odla-ai pm bug list --app <appId> --status open
48
+ npx odla-ai pm goal list --app <appId> --status open
49
+ npx odla-ai pm decision list --app <appId> --limit 20
50
+ ```
51
+
52
+ Read the active task, linked goal, and comments before changing code. Without
53
+ `--app`, list commands span every project the account co-owns, which is the
54
+ right first view when coordinating several efforts; scope every write to the
55
+ exact `appId` from that project's config. PM requires a registered app. Before
56
+ the first provision, use a focused commit/checkpoint, then initialize PM and
57
+ backfill that evidence immediately after registration. Never use a parallel
58
+ status diary or put a secret in PM.
59
+
40
60
  **Runbooks are procedure; JSDoc is API.** What a function takes, returns, and
41
61
  guarantees is documented on the export itself, ships in the installed
42
62
  `.d.ts`, and renders per package at <https://odla.ai/docs>. A runbook will tell
@@ -93,11 +113,14 @@ npx @odla-ai/cli init --app-id my-app --name "My App"
93
113
  ```
94
114
 
95
115
  For a project you keep working in, install it as a dev dependency so the
96
- shorter `npx odla-ai` form works and the version is pinned by your lockfile:
116
+ shorter `npx odla-ai` form works. ODLA is under active development: use a normal
117
+ dependency declaration, commit the lockfile, and let `npm ci` reproduce the
118
+ resolved graph:
97
119
 
98
120
  ```bash
99
- npm view @odla-ai/cli@0.16.6 version
100
- npm i -D --save-exact @odla-ai/cli@0.16.6
121
+ npm view @odla-ai/cli version
122
+ npm i -D @odla-ai/cli
123
+ npm ls @odla-ai/cli
101
124
  ```
102
125
 
103
126
  ## Prerequisites
@@ -360,8 +383,8 @@ rolling-window/age-out, quota-backoff, retention/purge, or provider-derived
360
383
  mutation-id control. Public health remains aggregate rather than per calendar.
361
384
 
362
385
  The bundled build/migration skills add a passive pre-ship gate with
363
- `@odla-ai/security`: after the exact-version availability check, run
364
- `npm i -D --save-exact @odla-ai/security@0.3.1`, then
386
+ `@odla-ai/security`: install it with a normal development range, commit the
387
+ lockfile, then run `npm i -D @odla-ai/security`, followed by
365
388
  `npx odla-security scan . --profile odla --out .odla/security/pre-ship --fail-on high --fail-on-candidates critical`.
366
389
  The passive scan remains a separate binary: it makes no model calls and does
367
390
  not execute target code. After explicit redacted-source approval,
@@ -385,12 +408,11 @@ a conservative `Retry-After`. A provider-side `429` uses the sanitized
385
408
  carries a bounded `Retry-After` only when the upstream supplied one. The CLI
386
409
  does not echo arbitrary response text or silently change the admin-selected
387
410
  model.
388
- Before installing the exact release, require
389
- `npm view @odla-ai/security@0.3.1 version` to succeed before installation. An
390
- exact-version `E404` means the release is unavailable, not that the security
391
- preflight passed, and does not prove the package name is absent. odla's own
392
- engineering environment uses an equivalent internal gate; customer projects
393
- should use the published `odla-security` command above.
411
+ Confirm `npm view @odla-ai/security version` succeeds and record the installed
412
+ version from `npm ls @odla-ai/security` with the scan evidence. A registry
413
+ failure blocks the preflight; it is not a clean scan. odla's own engineering
414
+ environment uses an equivalent internal gate; customer projects should use the
415
+ published `odla-security` command above.
394
416
 
395
417
  For a server-side, commit-pinned review, connect one repository through the
396
418
  source-read-only odla GitHub App. The CLI infers `owner/name` from a safe GitHub HTTPS
package/dist/bin.cjs CHANGED
@@ -7799,13 +7799,154 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
7799
7799
  }
7800
7800
  }
7801
7801
 
7802
+ // src/record.ts
7803
+ var import_node_fs13 = require("fs");
7804
+ var import_node_process8 = __toESM(require("process"), 1);
7805
+
7806
+ // src/surface.ts
7807
+ var PM_ACTIONS = {
7808
+ list: {},
7809
+ add: {},
7810
+ create: {},
7811
+ get: {},
7812
+ set: {},
7813
+ update: {},
7814
+ status: {},
7815
+ move: {},
7816
+ done: {},
7817
+ comment: {},
7818
+ comments: {},
7819
+ rm: {},
7820
+ delete: {}
7821
+ };
7822
+ var PM_ENTITIES = Object.fromEntries(
7823
+ ["goal", "conformance", "task", "kanban", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
7824
+ );
7825
+ var COMMAND_SURFACE = {
7826
+ admin: {
7827
+ ai: {
7828
+ show: {},
7829
+ set: {},
7830
+ credentials: {},
7831
+ models: {},
7832
+ usage: {},
7833
+ audit: {},
7834
+ credential: { set: {} }
7835
+ }
7836
+ },
7837
+ app: {
7838
+ archive: {},
7839
+ restore: {},
7840
+ export: {},
7841
+ import: {},
7842
+ rename: {},
7843
+ "refresh-sandbox": {},
7844
+ "go-live": {},
7845
+ promote: {},
7846
+ owners: { list: {}, add: {}, remove: {} }
7847
+ },
7848
+ calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
7849
+ capabilities: {},
7850
+ code: { connect: {} },
7851
+ doctor: {},
7852
+ help: {},
7853
+ init: {},
7854
+ pm: PM_ENTITIES,
7855
+ provision: {},
7856
+ runbook: {
7857
+ ask: {},
7858
+ search: {},
7859
+ impact: {},
7860
+ list: {},
7861
+ get: {},
7862
+ cat: {},
7863
+ new: {},
7864
+ edit: {},
7865
+ comment: {},
7866
+ import: {},
7867
+ visibility: {},
7868
+ publish: {},
7869
+ archive: {},
7870
+ history: {},
7871
+ revert: {},
7872
+ rm: {},
7873
+ lint: {}
7874
+ },
7875
+ secrets: { push: {}, set: {}, "set-clerk-key": {} },
7876
+ security: {
7877
+ plan: {},
7878
+ sources: {},
7879
+ run: {},
7880
+ status: {},
7881
+ report: {},
7882
+ github: { connect: {}, disconnect: {} }
7883
+ },
7884
+ setup: {},
7885
+ skill: { install: {} },
7886
+ smoke: {},
7887
+ version: {},
7888
+ whoami: {}
7889
+ };
7890
+ function acceptedAfter(path) {
7891
+ let node = COMMAND_SURFACE;
7892
+ for (const word of path) {
7893
+ node = node?.[word];
7894
+ if (!node) return [];
7895
+ }
7896
+ return Object.keys(node).sort();
7897
+ }
7898
+ function validateInvocation(words2) {
7899
+ let node = COMMAND_SURFACE;
7900
+ const walked = [];
7901
+ for (const word of words2) {
7902
+ if (Object.keys(node).length === 0) return null;
7903
+ const next = node[word];
7904
+ if (!next) return { validPrefix: walked.join(" "), word, accepted: Object.keys(node).sort() };
7905
+ walked.push(word);
7906
+ node = next;
7907
+ }
7908
+ return null;
7909
+ }
7910
+ function describeProblem(problem) {
7911
+ const where = problem.validPrefix ? `after "${problem.validPrefix}"` : "as a command";
7912
+ return `"${problem.word}" is not accepted ${where} \u2014 try: ${problem.accepted.join(", ")}`;
7913
+ }
7914
+ function invocationPath(words2) {
7915
+ let node = COMMAND_SURFACE;
7916
+ const path = [];
7917
+ for (const word of words2) {
7918
+ const next = node[word];
7919
+ if (!next) break;
7920
+ path.push(word);
7921
+ node = next;
7922
+ if (Object.keys(node).length === 0) break;
7923
+ }
7924
+ return path;
7925
+ }
7926
+
7927
+ // src/record.ts
7928
+ function recordInvocation(parsed) {
7929
+ const file = import_node_process8.default.env.ODLA_CLI_RECORD;
7930
+ if (!file) return;
7931
+ try {
7932
+ const entry = {
7933
+ path: invocationPath(parsed.positionals),
7934
+ options: Object.keys(parsed.options).sort()
7935
+ };
7936
+ if (!entry.path.length) return;
7937
+ (0, import_node_fs13.appendFileSync)(file, `${JSON.stringify(entry)}
7938
+ `);
7939
+ } catch {
7940
+ }
7941
+ }
7942
+
7802
7943
  // src/runbook-command.ts
7803
- var import_node_fs17 = require("fs");
7944
+ var import_node_fs18 = require("fs");
7804
7945
  var import_node_path14 = require("path");
7805
- var import_node_process9 = __toESM(require("process"), 1);
7946
+ var import_node_process10 = __toESM(require("process"), 1);
7806
7947
 
7807
7948
  // src/runbook-actions.ts
7808
- var import_node_fs13 = require("fs");
7949
+ var import_node_fs14 = require("fs");
7809
7950
 
7810
7951
  // src/runbook-requires.ts
7811
7952
  var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
@@ -7890,7 +8031,7 @@ async function bySlug(ctx, slug) {
7890
8031
  function readBody(file, inline) {
7891
8032
  if (inline !== void 0) return inline;
7892
8033
  if (file === void 0) throw new Error("supply the new text with --file <path>, --file - (stdin), or --body");
7893
- return (0, import_node_fs13.readFileSync)(file === "-" ? 0 : file, "utf8");
8034
+ return (0, import_node_fs14.readFileSync)(file === "-" ? 0 : file, "utf8");
7894
8035
  }
7895
8036
  var stamp = (ms) => ms ? new Date(ms).toISOString().slice(0, 16).replace("T", " ") : "";
7896
8037
  async function runbookList(ctx, all, query) {
@@ -7977,7 +8118,7 @@ async function runbookRemove(ctx, slug) {
7977
8118
  }
7978
8119
 
7979
8120
  // src/runbook-import.ts
7980
- var import_node_fs14 = require("fs");
8121
+ var import_node_fs15 = require("fs");
7981
8122
  var import_node_path11 = require("path");
7982
8123
  function parseRunbook(text, slug) {
7983
8124
  let rest = text;
@@ -8003,12 +8144,12 @@ function parseRunbook(text, slug) {
8003
8144
  };
8004
8145
  }
8005
8146
  function readRunbookDir(dir) {
8006
- if (!(0, import_node_fs14.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
8007
- const files = (0, import_node_fs14.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
8147
+ if (!(0, import_node_fs15.statSync)(dir, { throwIfNoEntry: false })?.isDirectory()) throw new Error(`not a directory: ${dir}`);
8148
+ const files = (0, import_node_fs15.readdirSync)(dir).filter((f) => f.endsWith(".md")).sort();
8008
8149
  if (!files.length) throw new Error(`no .md files in ${dir}`);
8009
8150
  return files.map((file) => {
8010
8151
  const slug = (0, import_node_path11.basename)(file, ".md");
8011
- const parsed = parseRunbook((0, import_node_fs14.readFileSync)((0, import_node_path11.join)(dir, file), "utf8"), slug);
8152
+ const parsed = parseRunbook((0, import_node_fs15.readFileSync)((0, import_node_path11.join)(dir, file), "utf8"), slug);
8012
8153
  return { file, slug, ...parsed, words: parsed.body.split(/\s+/).filter(Boolean).length };
8013
8154
  });
8014
8155
  }
@@ -8080,7 +8221,7 @@ async function upsert(ctx, r, visibility) {
8080
8221
 
8081
8222
  // src/runbook-impact.ts
8082
8223
  var import_node_child_process7 = require("child_process");
8083
- var import_node_fs15 = require("fs");
8224
+ var import_node_fs16 = require("fs");
8084
8225
  var import_node_path12 = require("path");
8085
8226
 
8086
8227
  // src/runbook-impact-scan.ts
@@ -8251,9 +8392,9 @@ ${body.split("\n").map((line) => `+${line}`).join("\n")}
8251
8392
  function manifestLabeller(root) {
8252
8393
  return (workspace) => {
8253
8394
  const manifest = (0, import_node_path12.join)(root, workspace, "package.json");
8254
- if (!(0, import_node_fs15.existsSync)(manifest)) return void 0;
8395
+ if (!(0, import_node_fs16.existsSync)(manifest)) return void 0;
8255
8396
  try {
8256
- const name = JSON.parse((0, import_node_fs15.readFileSync)(manifest, "utf8")).name;
8397
+ const name = JSON.parse((0, import_node_fs16.readFileSync)(manifest, "utf8")).name;
8257
8398
  return typeof name === "string" ? name : void 0;
8258
8399
  } catch {
8259
8400
  return void 0;
@@ -8320,7 +8461,7 @@ function report2(ctx, impacts) {
8320
8461
  async function runbookImpact(ctx, options, deps = {}) {
8321
8462
  const cwd = deps.cwd ?? process.cwd();
8322
8463
  const runGit = deps.runGit ?? gitRunner(cwd);
8323
- const read2 = deps.readRepoFile ?? ((path) => (0, import_node_fs15.readFileSync)((0, import_node_path12.join)(cwd, path), "utf8"));
8464
+ const read2 = deps.readRepoFile ?? ((path) => (0, import_node_fs16.readFileSync)((0, import_node_path12.join)(cwd, path), "utf8"));
8324
8465
  const surfaces = changedSurfaces(collectDiff(runGit, options.base, read2), manifestLabeller(cwd));
8325
8466
  if (!surfaces.length) {
8326
8467
  return ctx.out.log(
@@ -8332,115 +8473,6 @@ async function runbookImpact(ctx, options, deps = {}) {
8332
8473
  report2(ctx, impacts);
8333
8474
  }
8334
8475
 
8335
- // src/surface.ts
8336
- var PM_ACTIONS = {
8337
- list: {},
8338
- add: {},
8339
- create: {},
8340
- get: {},
8341
- set: {},
8342
- update: {},
8343
- status: {},
8344
- move: {},
8345
- done: {},
8346
- comment: {},
8347
- comments: {},
8348
- rm: {},
8349
- delete: {}
8350
- };
8351
- var PM_ENTITIES = Object.fromEntries(
8352
- ["goal", "conformance", "task", "kanban", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
8353
- );
8354
- var COMMAND_SURFACE = {
8355
- admin: {
8356
- ai: {
8357
- show: {},
8358
- set: {},
8359
- credentials: {},
8360
- models: {},
8361
- usage: {},
8362
- audit: {},
8363
- credential: { set: {} }
8364
- }
8365
- },
8366
- app: {
8367
- archive: {},
8368
- restore: {},
8369
- export: {},
8370
- import: {},
8371
- rename: {},
8372
- "refresh-sandbox": {},
8373
- "go-live": {},
8374
- promote: {},
8375
- owners: { list: {}, add: {}, remove: {} }
8376
- },
8377
- calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
8378
- capabilities: {},
8379
- code: { connect: {} },
8380
- doctor: {},
8381
- help: {},
8382
- init: {},
8383
- pm: PM_ENTITIES,
8384
- provision: {},
8385
- runbook: {
8386
- ask: {},
8387
- search: {},
8388
- impact: {},
8389
- list: {},
8390
- get: {},
8391
- cat: {},
8392
- new: {},
8393
- edit: {},
8394
- comment: {},
8395
- import: {},
8396
- visibility: {},
8397
- publish: {},
8398
- archive: {},
8399
- history: {},
8400
- revert: {},
8401
- rm: {},
8402
- lint: {}
8403
- },
8404
- secrets: { push: {}, set: {}, "set-clerk-key": {} },
8405
- security: {
8406
- plan: {},
8407
- sources: {},
8408
- run: {},
8409
- status: {},
8410
- report: {},
8411
- github: { connect: {}, disconnect: {} }
8412
- },
8413
- setup: {},
8414
- skill: { install: {} },
8415
- smoke: {},
8416
- version: {},
8417
- whoami: {}
8418
- };
8419
- function acceptedAfter(path) {
8420
- let node = COMMAND_SURFACE;
8421
- for (const word of path) {
8422
- node = node?.[word];
8423
- if (!node) return [];
8424
- }
8425
- return Object.keys(node).sort();
8426
- }
8427
- function validateInvocation(words2) {
8428
- let node = COMMAND_SURFACE;
8429
- const walked = [];
8430
- for (const word of words2) {
8431
- if (Object.keys(node).length === 0) return null;
8432
- const next = node[word];
8433
- if (!next) return { validPrefix: walked.join(" "), word, accepted: Object.keys(node).sort() };
8434
- walked.push(word);
8435
- node = next;
8436
- }
8437
- return null;
8438
- }
8439
- function describeProblem(problem) {
8440
- const where = problem.validPrefix ? `after "${problem.validPrefix}"` : "as a command";
8441
- return `"${problem.word}" is not accepted ${where} \u2014 try: ${problem.accepted.join(", ")}`;
8442
- }
8443
-
8444
8476
  // src/runbook-lint.ts
8445
8477
  function invocationsIn(body) {
8446
8478
  const re = /(`|npx[^\S\n]+)?(?:@odla-ai\/cli(?:@[\w.-]+)?|odla-ai)((?:[^\S\n]+[a-z][\w-]*)+)/g;
@@ -8562,12 +8594,12 @@ async function runbookComment(ctx, slug, body) {
8562
8594
 
8563
8595
  // src/runbook-editor.ts
8564
8596
  var import_node_child_process8 = require("child_process");
8565
- var import_node_fs16 = require("fs");
8597
+ var import_node_fs17 = require("fs");
8566
8598
  var import_node_os4 = require("os");
8567
8599
  var import_node_path13 = require("path");
8568
- var import_node_process8 = __toESM(require("process"), 1);
8600
+ var import_node_process9 = __toESM(require("process"), 1);
8569
8601
  var EDITOR_ENV = ["ODLA_EDITOR", "VISUAL", "EDITOR"];
8570
- function resolveEditor(env = import_node_process8.default.env) {
8602
+ function resolveEditor(env = import_node_process9.default.env) {
8571
8603
  for (const name of EDITOR_ENV) {
8572
8604
  const value = env[name];
8573
8605
  if (value && value.trim()) return value.trim();
@@ -8581,8 +8613,8 @@ function defaultRun(command, path) {
8581
8613
  return result.status ?? 0;
8582
8614
  }
8583
8615
  function editText(initial, slug, deps = {}) {
8584
- const env = deps.env ?? import_node_process8.default.env;
8585
- const interactive = deps.interactive ?? (() => Boolean(import_node_process8.default.stdin.isTTY));
8616
+ const env = deps.env ?? import_node_process9.default.env;
8617
+ const interactive = deps.interactive ?? (() => Boolean(import_node_process9.default.stdin.isTTY));
8586
8618
  const editor = resolveEditor(env);
8587
8619
  if (!editor)
8588
8620
  throw new Error(
@@ -8590,16 +8622,16 @@ function editText(initial, slug, deps = {}) {
8590
8622
  );
8591
8623
  if (!interactive())
8592
8624
  throw new Error(`cannot open an editor without a terminal \u2014 pass --file <path> or --body "\u2026" instead`);
8593
- const dir = (0, import_node_fs16.mkdtempSync)((0, import_node_path13.join)((0, import_node_os4.tmpdir)(), "odla-runbook-"));
8625
+ const dir = (0, import_node_fs17.mkdtempSync)((0, import_node_path13.join)((0, import_node_os4.tmpdir)(), "odla-runbook-"));
8594
8626
  const file = (0, import_node_path13.join)(dir, `${slug}.md`);
8595
8627
  try {
8596
- (0, import_node_fs16.writeFileSync)(file, initial, { mode: 384 });
8628
+ (0, import_node_fs17.writeFileSync)(file, initial, { mode: 384 });
8597
8629
  const code = defaultRunOrInjected(deps)(editor, file);
8598
8630
  if (code !== 0) throw new Error(`editor "${editor}" exited with ${code}; nothing was written`);
8599
- const edited = (0, import_node_fs16.readFileSync)(file, "utf8");
8631
+ const edited = (0, import_node_fs17.readFileSync)(file, "utf8");
8600
8632
  return edited === initial ? null : edited;
8601
8633
  } finally {
8602
- (0, import_node_fs16.rmSync)(dir, { recursive: true, force: true });
8634
+ (0, import_node_fs17.rmSync)(dir, { recursive: true, force: true });
8603
8635
  }
8604
8636
  }
8605
8637
  var defaultRunOrInjected = (deps) => deps.run ?? defaultRun;
@@ -8699,11 +8731,11 @@ var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visi
8699
8731
  var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact", "lint"]);
8700
8732
  async function loadOrDefaultConfig(configPath, action, appId) {
8701
8733
  const projectScoped = appId !== void 0;
8702
- if (!projectScoped && CONFIG_FREE.has(action) && !(0, import_node_fs17.existsSync)((0, import_node_path14.resolve)(configPath))) {
8703
- const rootDir = import_node_process9.default.cwd();
8734
+ if (!projectScoped && CONFIG_FREE.has(action) && !(0, import_node_fs18.existsSync)((0, import_node_path14.resolve)(configPath))) {
8735
+ const rootDir = import_node_process10.default.cwd();
8704
8736
  return {
8705
8737
  configPath,
8706
- platformUrl: import_node_process9.default.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
8738
+ platformUrl: import_node_process10.default.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
8707
8739
  rootDir,
8708
8740
  app: { id: "", name: "" },
8709
8741
  local: { tokenFile: (0, import_node_path14.join)(rootDir, ".odla/dev-token.json") }
@@ -9553,6 +9585,7 @@ function exitCodeFor(err) {
9553
9585
  }
9554
9586
  async function runCli(argv = process.argv.slice(2), dependencies = {}) {
9555
9587
  const parsed = parseArgv(argv);
9588
+ recordInvocation(parsed);
9556
9589
  const command = parsed.positionals[0] ?? "help";
9557
9590
  if (command === "version" || command === "-v" || parsed.options.version === true) {
9558
9591
  assertArgs(parsed, ["version"], 1);