motifcode 0.2.0 → 0.2.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
@@ -13,11 +13,13 @@ failure handling follow from what is measurably true about Motif-3.
13
13
  > Technologies or by Infron. *Motif* and *Motif-3* are their names; this package is a client of the model.
14
14
 
15
15
  ```bash
16
- npm install -g motifcode # or: npx motifcode
17
16
  cd your-project
18
- motif # the first session asks for your Infron API key and saves it to ~/.motif/.env
17
+ npx motifcode # asks for your Infron API key once, then offers to install the `motif` command
19
18
  ```
20
19
 
20
+ Or `npm install -g motifcode` directly; either way `motif` (or `motifcode`) opens the session
21
+ from any folder afterwards, with the key saved in `~/.motif/.env`.
22
+
21
23
  1. Sign in at [infron.ai/login](https://infron.ai/login), open [Dashboard → API Keys](https://infron.ai/dashboard/apiKeys), click **Add new key**.
22
24
  2. Run `motif` and paste the key when asked. It is checked against the endpoint, saved to
23
25
  `~/.motif/.env` (readable only by you), and never shown to the model. `motif login` does the
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.2.0",
3
- "bytes": 436392,
2
+ "version": "0.2.1",
3
+ "bytes": 440653,
4
4
  "node": "v24.13.0"
5
5
  }
package/dist/motif.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // packages/cli/src/main.ts
4
- import { existsSync as existsSync9, readFileSync as readFileSync9, readdirSync as readdirSync4 } from "node:fs";
4
+ import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync4 } from "node:fs";
5
5
  import { homedir as homedir5 } from "node:os";
6
- import { join as join9 } from "node:path";
6
+ import { join as join10 } from "node:path";
7
7
 
8
8
  // packages/tools/src/schemas.ts
9
9
  var CORE_TOOLS = Object.freeze([
@@ -470,7 +470,7 @@ function openStringArray(block, arrayKeys) {
470
470
  if (arrayKeys.size === 0) return block;
471
471
  const alt = [...arrayKeys].map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
472
472
  const re = new RegExp(`"(${alt})"(\\s*:\\s*)"`, "g");
473
- return block.replace(re, (_m, key, sep2) => `"${key}"${sep2}["`);
473
+ return block.replace(re, (_m, key, sep3) => `"${key}"${sep3}["`);
474
474
  }
475
475
  function balanceBracketsDetailed(block) {
476
476
  const CLOSER = { "{": "}", "[": "]" };
@@ -3698,7 +3698,7 @@ var RULES = [
3698
3698
  // redact commit hashes, base64 test fixtures and half of a lockfile.
3699
3699
  name: "secret-assignment",
3700
3700
  pattern: /\b([A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|API_KEY|APIKEY|ACCESS_KEY|PRIVATE_KEY)[A-Z0-9_]*)(\s*[=:]\s*)(?:"([^"]*)"|'([^']*)'|(\S+))/g,
3701
- replace: (_m, name, sep2) => `${name}${sep2}${MASK}`
3701
+ replace: (_m, name, sep3) => `${name}${sep3}${MASK}`
3702
3702
  }
3703
3703
  ];
3704
3704
  function redactText(text) {
@@ -6217,8 +6217,8 @@ var Screen = class {
6217
6217
  };
6218
6218
 
6219
6219
  // packages/cli/src/chat.ts
6220
- import { appendFileSync as appendFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
6221
- import { dirname as dirname5, join as join6, resolve as resolve5 } from "node:path";
6220
+ import { appendFileSync as appendFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
6221
+ import { dirname as dirname5, join as join7, resolve as resolve5 } from "node:path";
6222
6222
 
6223
6223
  // packages/cli/src/files.ts
6224
6224
  import { execFileSync } from "node:child_process";
@@ -6347,6 +6347,24 @@ function expandMentions(text, mentions, opts) {
6347
6347
  ${blocks.join("\n\n")}`, attached };
6348
6348
  }
6349
6349
 
6350
+ // packages/cli/src/install.ts
6351
+ import { existsSync as existsSync5 } from "node:fs";
6352
+ import { delimiter, join as join5, sep as sep2 } from "node:path";
6353
+ function ranFromNpx(entry) {
6354
+ if (!entry) return false;
6355
+ return entry.split(/[\\/]/).includes("_npx");
6356
+ }
6357
+ function commandOnPath(name, pathEnv = process.env["PATH"] ?? "") {
6358
+ for (const dir of pathEnv.split(delimiter)) {
6359
+ if (dir === "" || dir.split(sep2).includes("_npx") || dir.includes(`${sep2}_npx${sep2}`) || dir.includes("/_npx/")) continue;
6360
+ if (existsSync5(join5(dir, name)) || existsSync5(join5(dir, `${name}.cmd`))) return true;
6361
+ }
6362
+ return false;
6363
+ }
6364
+ function installCommand(version) {
6365
+ return `npm install -g motifcode@${version}`;
6366
+ }
6367
+
6350
6368
  // packages/cli/src/login.ts
6351
6369
  import { homedir as homedir3 } from "node:os";
6352
6370
  var KEY_PAGE = "https://infron.ai/dashboard/apiKeys";
@@ -6765,7 +6783,7 @@ async function runSlash(text, ctx) {
6765
6783
  import { execFileSync as execFileSync2 } from "node:child_process";
6766
6784
  import { mkdtempSync, realpathSync as realpathSync2 } from "node:fs";
6767
6785
  import { platform, tmpdir } from "node:os";
6768
- import { join as join5 } from "node:path";
6786
+ import { join as join6 } from "node:path";
6769
6787
  function has(binary) {
6770
6788
  try {
6771
6789
  execFileSync2("/bin/sh", ["-c", `command -v ${binary}`], { stdio: "ignore" });
@@ -6856,7 +6874,7 @@ function detectSandbox() {
6856
6874
  return cached;
6857
6875
  }
6858
6876
  function makeScratch() {
6859
- return mkdtempSync(join5(tmpdir(), "motif-scratch-"));
6877
+ return mkdtempSync(join6(tmpdir(), "motif-scratch-"));
6860
6878
  }
6861
6879
 
6862
6880
  // packages/cli/src/doctor.ts
@@ -7096,7 +7114,7 @@ function worstState(checks) {
7096
7114
 
7097
7115
  // packages/cli/src/executor.ts
7098
7116
  import { spawn as spawn2 } from "node:child_process";
7099
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
7117
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
7100
7118
  import { dirname as dirname4, resolve as resolve4 } from "node:path";
7101
7119
 
7102
7120
  // packages/cli/src/policy.ts
@@ -7359,7 +7377,7 @@ function writeFile(path, content, cwd) {
7359
7377
  try {
7360
7378
  const target = resolve4(cwd, path);
7361
7379
  mkdirSync4(dirname4(target), { recursive: true });
7362
- const existed = existsSync5(target);
7380
+ const existed = existsSync6(target);
7363
7381
  writeFileSync3(target, content, "utf8");
7364
7382
  const lines = content === "" ? 0 : content.split("\n").length;
7365
7383
  return {
@@ -7676,6 +7694,8 @@ var Chat = class {
7676
7694
  shellBusy = false;
7677
7695
  /** A tool call waiting for the person's yes or no, and which of the three answers is selected. */
7678
7696
  pendingConfirm = null;
7697
+ /** A question with numbered answers in place of the prompt, and which one is selected. */
7698
+ pendingChoice = null;
7679
7699
  /** A secret being typed in place of the prompt — the API key at login. */
7680
7700
  pendingSecret = null;
7681
7701
  /** The credential for this session. Starts as the caller's; `/login` replaces it, `/logout` drops it. */
@@ -7752,6 +7772,7 @@ var Chat = class {
7752
7772
  this.screen.append({ kind: "system", title: "login", lines: await this.login("startup") });
7753
7773
  this.refresh();
7754
7774
  }
7775
+ if (this.opts.offerInstall) await this.offerInstall();
7755
7776
  if (this.opts.continueFrom) {
7756
7777
  try {
7757
7778
  this.screen.append({ kind: "system", title: "continuing", lines: await this.resume(this.opts.continueFrom) });
@@ -7770,6 +7791,10 @@ var Chat = class {
7770
7791
  this.answerSecret(key);
7771
7792
  return;
7772
7793
  }
7794
+ if (this.pendingChoice) {
7795
+ this.answerChoice(key);
7796
+ return;
7797
+ }
7773
7798
  if (this.pendingConfirm) {
7774
7799
  this.answerConfirm(key);
7775
7800
  return;
@@ -8030,6 +8055,58 @@ var Chat = class {
8030
8055
  get needsLogin() {
8031
8056
  return this.opts.requireKey === true && this.apiKey === void 0;
8032
8057
  }
8058
+ /** Take over the prompt for one question; resolves with the index chosen, or null on esc. */
8059
+ askChoice(title, lines, options) {
8060
+ return new Promise((resolve6) => {
8061
+ this.pendingChoice = { title, lines, options, selected: 0, resolve: resolve6 };
8062
+ this.refresh();
8063
+ });
8064
+ }
8065
+ /** A number, or ↑↓ and Enter, picks an answer; Esc (and Ctrl-C, Ctrl-D) declines. Nothing else does anything. */
8066
+ answerChoice(key) {
8067
+ const pending = this.pendingChoice;
8068
+ if (!pending) return;
8069
+ const n = pending.options.length;
8070
+ let choice;
8071
+ if (key.type === "enter") choice = pending.selected;
8072
+ else if (key.type === "escape" || key.type === "ctrl" && (key.key === "c" || key.key === "d")) choice = null;
8073
+ else if (key.type === "up") pending.selected = (pending.selected + n - 1) % n;
8074
+ else if (key.type === "down") pending.selected = (pending.selected + 1) % n;
8075
+ else if (key.type === "text" && /^[1-9]$/.test(key.text) && Number(key.text) <= n) choice = Number(key.text) - 1;
8076
+ if (choice !== void 0) {
8077
+ this.pendingChoice = null;
8078
+ pending.resolve(choice);
8079
+ }
8080
+ this.refresh();
8081
+ }
8082
+ /**
8083
+ * `npx motifcode` runs the package without leaving a command behind, and
8084
+ * the first person to try it typed `motif` afterwards and found nothing.
8085
+ * So a run from npx, on a machine without `motif`, is offered the global
8086
+ * install once — npm's own, of exactly the version that is running — and
8087
+ * the session carries on either way.
8088
+ */
8089
+ async offerInstall() {
8090
+ const command = installCommand(this.opts.version);
8091
+ const choice = await this.askChoice(
8092
+ "Install the motif command?",
8093
+ ["This run came from npx, which leaves no command behind.", `${command} puts motif and motifcode on your PATH.`],
8094
+ ["1. Yes, install it now", "2. Not now \u2014 npx motifcode keeps working"]
8095
+ );
8096
+ if (choice !== 0) return;
8097
+ const id = `install-${++this.shellSequence}`;
8098
+ this.screen.append({ kind: "tool", id, name: "bash", args: { command }, repaired: false, hooks: [] });
8099
+ this.refresh();
8100
+ const r = await (this.opts.installGlobal ?? ((c) => runShell(c, { cwd: this.settings.cwd, timeoutMs: 18e4, outputCap: 2e4 })))(command);
8101
+ const ok2 = r.code === 0 && !r.timedOut;
8102
+ const output = r.timedOut ? `${r.output.trim()}
8103
+ (killed after ${Math.round(r.ms / 1e3)}s)`.trim() : r.output.trim() || `(exit ${r.code})`;
8104
+ this.screen.apply({ type: "tool_end", id, ok: ok2, output, ms: r.ms });
8105
+ this.screen.append(
8106
+ ok2 ? { kind: "notice", level: "info", text: "installed: from now on `motif` (or `motifcode`) opens this from any folder; this session carries on" } : { kind: "notice", level: "warn", text: `the install did not finish; run \`${command}\` yourself (with sudo if npm's global folder is not yours), or keep using npx motifcode` }
8107
+ );
8108
+ this.refresh();
8109
+ }
8033
8110
  /** Take over the prompt for one secret; resolves with the text, or null when given up. */
8034
8111
  askSecret(title, lines, prompt, cancelHint) {
8035
8112
  return new Promise((resolve6) => {
@@ -8171,14 +8248,16 @@ var Chat = class {
8171
8248
  draft: this.composer.snapshot(),
8172
8249
  placeholder: PLACEHOLDER,
8173
8250
  ...this.pendingConfirm ? { confirm: this.confirmView(this.pendingConfirm.call, this.pendingConfirm.selected) } : {},
8251
+ ...this.pendingChoice ? { confirm: { title: this.pendingChoice.title, lines: this.pendingChoice.lines, choices: this.pendingChoice.options.map((o, i) => `${i === this.pendingChoice.selected ? "\u276F" : " "} ${o}`) } } : {},
8174
8252
  ...this.pendingSecret ? { secret: { title: this.pendingSecret.title, lines: this.pendingSecret.lines, prompt: this.pendingSecret.prompt } } : {},
8175
- ...items.length > 0 && !this.pendingConfirm && !this.pendingSecret ? { menu: { items, selected: clampSelection(this.menuSelected, items.length), prefix: this.mentionOpen() ? "@" : "/" } } : {}
8253
+ ...items.length > 0 && !this.pendingConfirm && !this.pendingSecret && !this.pendingChoice ? { menu: { items, selected: clampSelection(this.menuSelected, items.length), prefix: this.mentionOpen() ? "@" : "/" } } : {}
8176
8254
  };
8177
8255
  this.screen.setHint(this.hintText());
8178
8256
  this.screen.setComposer(view);
8179
8257
  }
8180
8258
  hintText() {
8181
8259
  if (this.pendingSecret) return this.pendingSecret.cancelHint;
8260
+ if (this.pendingChoice) return "1 2 or \u2191\u2193 enter \xB7 esc leaves it";
8182
8261
  if (this.pendingConfirm) return "1 2 3 or \u2191\u2193 enter \xB7 esc declines";
8183
8262
  if (this.ctrlCArmedAt > 0 && this.now() - this.ctrlCArmedAt <= CTRL_C_WINDOW_MS) return "ctrl-c again to quit";
8184
8263
  if (this.queued.length > 0) {
@@ -8294,12 +8373,12 @@ ${output.slice(0, 8e3)}
8294
8373
  * is built per task.
8295
8374
  */
8296
8375
  addNote(note) {
8297
- const path = this.opts.notesPath ?? join6(this.settings.cwd, ".motif", "NOTES.md");
8376
+ const path = this.opts.notesPath ?? join7(this.settings.cwd, ".motif", "NOTES.md");
8298
8377
  try {
8299
8378
  mkdirSync5(dirname5(path), { recursive: true, mode: 448 });
8300
- const existing = existsSync6(path) ? readFileSync6(path, "utf8") : "";
8301
- const sep2 = existing === "" || existing.endsWith("\n") ? "" : "\n";
8302
- appendFileSync2(path, `${sep2}- ${note}
8379
+ const existing = existsSync7(path) ? readFileSync6(path, "utf8") : "";
8380
+ const sep3 = existing === "" || existing.endsWith("\n") ? "" : "\n";
8381
+ appendFileSync2(path, `${sep3}- ${note}
8303
8382
  `, "utf8");
8304
8383
  this.projectNotes = readFileSync6(path, "utf8");
8305
8384
  this.screen.append({ kind: "notice", level: "info", text: `noted in ${path}; the next task reads it` });
@@ -8322,7 +8401,7 @@ ${output.slice(0, 8e3)}
8322
8401
  this.tasks.push(task);
8323
8402
  const transport = (this.opts.makeTransport ?? defaultTransport)(this.settings, this.apiKey);
8324
8403
  const runId = new Date(this.now()).toISOString().replace(/[:.]/g, "-") + `-${String(this.totals.tasks + 1).padStart(2, "0")}`;
8325
- const journalPath = join6(this.opts.journalDir, `${runId}.jsonl`);
8404
+ const journalPath = join7(this.opts.journalDir, `${runId}.jsonl`);
8326
8405
  const scope = { scopeId: "root", scopeKind: "root" };
8327
8406
  const journal = new Journal(
8328
8407
  journalPath,
@@ -8681,8 +8760,8 @@ ${output.slice(0, 8e3)}
8681
8760
  },
8682
8761
  compact: (focus) => this.compactNow(focus),
8683
8762
  notes: () => {
8684
- const path = this.opts.notesPath ?? join6(this.settings.cwd, ".motif", "NOTES.md");
8685
- if (!existsSync6(path)) return [`no notes yet; # <text> at the prompt writes ${path}`];
8763
+ const path = this.opts.notesPath ?? join7(this.settings.cwd, ".motif", "NOTES.md");
8764
+ if (!existsSync7(path)) return [`no notes yet; # <text> at the prompt writes ${path}`];
8686
8765
  return [path, "", ...readFileSync6(path, "utf8").replace(/\s+$/, "").split("\n")];
8687
8766
  },
8688
8767
  hooks: () => this.opts.hookLines ?? ["no hooks"],
@@ -8744,8 +8823,8 @@ ${output.slice(0, 8e3)}
8744
8823
  const lines = rows.map(([k, v, from]) => `${k.padEnd(16)} ${v.padEnd(40)} ${from}`);
8745
8824
  lines.push("");
8746
8825
  if (info) {
8747
- lines.push(`user file ${info.userPath}${existsSync6(info.userPath) ? "" : " (absent)"}`);
8748
- const projectState = !existsSync6(info.projectPath) ? " (absent)" : info.projectApplied ? " (applied)" : " (present, not trusted \u2014 run `motif trust`)";
8826
+ lines.push(`user file ${info.userPath}${existsSync7(info.userPath) ? "" : " (absent)"}`);
8827
+ const projectState = !existsSync7(info.projectPath) ? " (absent)" : info.projectApplied ? " (applied)" : " (present, not trusted \u2014 run `motif trust`)";
8749
8828
  lines.push(`project file ${info.projectPath}${projectState}`);
8750
8829
  }
8751
8830
  lines.push("flags and MOTIF_* in the environment or .env outrank both files; /model and the rest save to the user file");
@@ -8800,7 +8879,7 @@ ${output.slice(0, 8e3)}
8800
8879
  file = picked.path;
8801
8880
  }
8802
8881
  const path = resolve5(this.settings.cwd, file);
8803
- if (!existsSync6(path)) throw new Error(`${path} does not exist`);
8882
+ if (!existsSync7(path)) throw new Error(`${path} does not exist`);
8804
8883
  const state = loadResume(path);
8805
8884
  if (state.corruption !== void 0) throw new Error(`this journal is corrupt (${state.corruption})`);
8806
8885
  if (!state.checkpoint) throw new Error("no checkpoint was written in that session; there is nothing to continue");
@@ -8822,7 +8901,7 @@ ${output.slice(0, 8e3)}
8822
8901
  }
8823
8902
  setCwd(path) {
8824
8903
  const target = resolve5(this.settings.cwd, path);
8825
- if (!existsSync6(target) || !statSync3(target).isDirectory()) return [`not a directory: ${target}`];
8904
+ if (!existsSync7(target) || !statSync3(target).isDirectory()) return [`not a directory: ${target}`];
8826
8905
  if (this.active) return ["a task is running; wait for it or interrupt it first"];
8827
8906
  this.executor.close();
8828
8907
  this.settings.cwd = target;
@@ -8839,7 +8918,7 @@ Input from the person:
8839
8918
  ${input}`;
8840
8919
  }
8841
8920
  function readHistory(path) {
8842
- if (!existsSync6(path)) return [];
8921
+ if (!existsSync7(path)) return [];
8843
8922
  const entries = [];
8844
8923
  for (const line2 of readFileSync6(path, "utf8").split("\n")) {
8845
8924
  if (!line2.trim()) continue;
@@ -8867,11 +8946,11 @@ function defaultTransport(settings, apiKey) {
8867
8946
  }
8868
8947
 
8869
8948
  // packages/cli/src/plugins.ts
8870
- import { existsSync as existsSync7, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
8871
- import { join as join7 } from "node:path";
8949
+ import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
8950
+ import { join as join8 } from "node:path";
8872
8951
  function readManifest(dir) {
8873
- const file = join7(dir, "plugin.json");
8874
- if (!existsSync7(file)) return null;
8952
+ const file = join8(dir, "plugin.json");
8953
+ if (!existsSync8(file)) return null;
8875
8954
  const raw = JSON.parse(readFileSync7(file, "utf8"));
8876
8955
  const name = typeof raw["name"] === "string" && raw["name"] !== "" ? raw["name"] : null;
8877
8956
  if (!name) throw new Error(`${file}: plugin.json needs a name`);
@@ -8883,10 +8962,10 @@ function readManifest(dir) {
8883
8962
  }
8884
8963
  function loadPluginsFrom(root, source) {
8885
8964
  const out = { plugins: [], skills: [], agents: [], problems: [] };
8886
- const dir = join7(root, "plugins");
8887
- if (!existsSync7(dir)) return out;
8965
+ const dir = join8(root, "plugins");
8966
+ if (!existsSync8(dir)) return out;
8888
8967
  for (const name of readdirSync3(dir).sort()) {
8889
- const path = join7(dir, name);
8968
+ const path = join8(dir, name);
8890
8969
  let st;
8891
8970
  try {
8892
8971
  st = statSync4(path);
@@ -8903,11 +8982,11 @@ function loadPluginsFrom(root, source) {
8903
8982
  }
8904
8983
  if (!manifest) continue;
8905
8984
  const info = { ...manifest, path, source, skills: [], agents: [] };
8906
- const skillsDir = join7(path, "skills");
8907
- if (existsSync7(skillsDir)) {
8985
+ const skillsDir = join8(path, "skills");
8986
+ if (existsSync8(skillsDir)) {
8908
8987
  for (const s of readdirSync3(skillsDir).sort()) {
8909
- const file = join7(skillsDir, s, "SKILL.md");
8910
- if (!existsSync7(file)) continue;
8988
+ const file = join8(skillsDir, s, "SKILL.md");
8989
+ if (!existsSync8(file)) continue;
8911
8990
  try {
8912
8991
  const skill = parseSkill(readFileSync7(file, "utf8"), source);
8913
8992
  out.skills.push(skill);
@@ -8917,11 +8996,11 @@ function loadPluginsFrom(root, source) {
8917
8996
  }
8918
8997
  }
8919
8998
  }
8920
- const agentsDir = join7(path, "agents");
8921
- if (existsSync7(agentsDir)) {
8999
+ const agentsDir = join8(path, "agents");
9000
+ if (existsSync8(agentsDir)) {
8922
9001
  for (const a of readdirSync3(agentsDir).sort()) {
8923
9002
  if (!a.endsWith(".md")) continue;
8924
- const file = join7(agentsDir, a);
9003
+ const file = join8(agentsDir, a);
8925
9004
  try {
8926
9005
  const agent = parseAgent(readFileSync7(file, "utf8"), source);
8927
9006
  out.agents.push(agent);
@@ -8936,8 +9015,8 @@ function loadPluginsFrom(root, source) {
8936
9015
  return out;
8937
9016
  }
8938
9017
  function loadPlugins(opts) {
8939
- const user = loadPluginsFrom(join7(opts.home, ".motif"), "user");
8940
- const project = loadPluginsFrom(join7(opts.cwd, ".motif"), "project");
9018
+ const user = loadPluginsFrom(join8(opts.home, ".motif"), "user");
9019
+ const project = loadPluginsFrom(join8(opts.cwd, ".motif"), "project");
8941
9020
  return {
8942
9021
  plugins: [...user.plugins, ...project.plugins],
8943
9022
  skills: [...user.skills, ...project.skills],
@@ -8955,9 +9034,9 @@ function describePlugins(loaded) {
8955
9034
  }
8956
9035
 
8957
9036
  // packages/cli/src/settings.ts
8958
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "node:fs";
9037
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "node:fs";
8959
9038
  import { homedir as homedir4 } from "node:os";
8960
- import { dirname as dirname6, join as join8 } from "node:path";
9039
+ import { dirname as dirname6, join as join9 } from "node:path";
8961
9040
  var KEYS2 = [
8962
9041
  "model",
8963
9042
  "endpoint",
@@ -8971,10 +9050,10 @@ var KEYS2 = [
8971
9050
  "permissions"
8972
9051
  ];
8973
9052
  function userSettingsPath(home = homedir4()) {
8974
- return join8(home, ".motif", "settings.json");
9053
+ return join9(home, ".motif", "settings.json");
8975
9054
  }
8976
9055
  function projectSettingsPath(cwd) {
8977
- return join8(cwd, ".motif", "settings.json");
9056
+ return join9(cwd, ".motif", "settings.json");
8978
9057
  }
8979
9058
  function parseSettings(text) {
8980
9059
  const values = {};
@@ -9030,7 +9109,7 @@ function loadSettings(opts) {
9030
9109
  const sources = {};
9031
9110
  let projectApplied = false;
9032
9111
  const apply = (path, source, gate) => {
9033
- if (!existsSync8(path)) return false;
9112
+ if (!existsSync9(path)) return false;
9034
9113
  const content = readFileSync8(path, "utf8");
9035
9114
  if (gate && !gate(content)) return false;
9036
9115
  const { values: found, problems } = parseSettings(content);
@@ -9050,7 +9129,7 @@ function loadSettings(opts) {
9050
9129
  function saveUserSetting(key, value, home = homedir4()) {
9051
9130
  const path = userSettingsPath(home);
9052
9131
  let current = {};
9053
- if (existsSync8(path)) {
9132
+ if (existsSync9(path)) {
9054
9133
  try {
9055
9134
  const parsed = JSON.parse(readFileSync8(path, "utf8"));
9056
9135
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) current = parsed;
@@ -9065,7 +9144,7 @@ function saveUserSetting(key, value, home = homedir4()) {
9065
9144
  }
9066
9145
 
9067
9146
  // packages/cli/src/main.ts
9068
- var VERSION = "0.2.0";
9147
+ var VERSION = "0.2.1";
9069
9148
  var SHORT_FLAGS = { p: "print", c: "continue", i: "interactive", v: "verbose", h: "help" };
9070
9149
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
9071
9150
  "print",
@@ -9147,13 +9226,13 @@ function loadSkills(cwd) {
9147
9226
  reg.registerAll(BUILTIN_SKILLS);
9148
9227
  reg.registerAll(plugins(cwd).skills);
9149
9228
  for (const [dir, source] of [
9150
- [join9(homedir5(), CONFIG_DIR, "skills"), "user"],
9151
- [join9(cwd, CONFIG_DIR, "skills"), "project"]
9229
+ [join10(homedir5(), CONFIG_DIR, "skills"), "user"],
9230
+ [join10(cwd, CONFIG_DIR, "skills"), "project"]
9152
9231
  ]) {
9153
- if (!existsSync9(dir)) continue;
9232
+ if (!existsSync10(dir)) continue;
9154
9233
  for (const name of readdirSync4(dir)) {
9155
- const file = join9(dir, name, "SKILL.md");
9156
- if (!existsSync9(file)) continue;
9234
+ const file = join10(dir, name, "SKILL.md");
9235
+ if (!existsSync10(file)) continue;
9157
9236
  try {
9158
9237
  reg.register(parseSkill(readFileSync9(file, "utf8"), source));
9159
9238
  } catch (err) {
@@ -9165,8 +9244,8 @@ function loadSkills(cwd) {
9165
9244
  return reg;
9166
9245
  }
9167
9246
  function loadHooks(cwd, opts) {
9168
- const file = join9(cwd, CONFIG_DIR, "settings.json");
9169
- if (!existsSync9(file)) return DEFAULT_HOOKS;
9247
+ const file = join10(cwd, CONFIG_DIR, "settings.json");
9248
+ if (!existsSync10(file)) return DEFAULT_HOOKS;
9170
9249
  const content = readFileSync9(file, "utf8");
9171
9250
  let parsed;
9172
9251
  try {
@@ -9204,13 +9283,13 @@ function loadAgents(cwd) {
9204
9283
  reg.registerAll(BUILTIN_AGENTS);
9205
9284
  reg.registerAll(plugins(cwd).agents);
9206
9285
  for (const [dir, source] of [
9207
- [join9(homedir5(), CONFIG_DIR, "agents"), "user"],
9208
- [join9(cwd, CONFIG_DIR, "agents"), "project"]
9286
+ [join10(homedir5(), CONFIG_DIR, "agents"), "user"],
9287
+ [join10(cwd, CONFIG_DIR, "agents"), "project"]
9209
9288
  ]) {
9210
- if (!existsSync9(dir)) continue;
9289
+ if (!existsSync10(dir)) continue;
9211
9290
  for (const name of readdirSync4(dir)) {
9212
9291
  if (!name.endsWith(".md")) continue;
9213
- const file = join9(dir, name);
9292
+ const file = join10(dir, name);
9214
9293
  try {
9215
9294
  reg.register(parseAgent(readFileSync9(file, "utf8"), source));
9216
9295
  } catch (err) {
@@ -9234,8 +9313,8 @@ function describeHooks(cwd, hooks) {
9234
9313
  for (const d of defs ?? []) lines.push(`${event.padEnd(12)} ${d.matcher ? `[${d.matcher}] ` : ""}${d.command}${d.blocking ? " (blocking)" : ""}`);
9235
9314
  }
9236
9315
  if (lines.length === 0) lines.push("no hooks configured");
9237
- const file = join9(cwd, CONFIG_DIR, "settings.json");
9238
- if (existsSync9(file)) {
9316
+ const file = join10(cwd, CONFIG_DIR, "settings.json");
9317
+ if (existsSync10(file)) {
9239
9318
  const content = readFileSync9(file, "utf8");
9240
9319
  const trusted = checkTrust(loadTrustStore(), cwd, content).trusted;
9241
9320
  lines.push("", `${file}: ${trusted ? "trusted" : "present but not trusted \u2014 its hooks and settings are not applied; run motif trust"}`);
@@ -9245,9 +9324,9 @@ function describeHooks(cwd, hooks) {
9245
9324
  return lines;
9246
9325
  }
9247
9326
  function loadProjectNotes(cwd) {
9248
- for (const name of ["AGENTS.md", "CLAUDE.md", join9(CONFIG_DIR, "NOTES.md")]) {
9249
- const file = join9(cwd, name);
9250
- if (existsSync9(file)) return readFileSync9(file, "utf8");
9327
+ for (const name of ["AGENTS.md", "CLAUDE.md", join10(CONFIG_DIR, "NOTES.md")]) {
9328
+ const file = join10(cwd, name);
9329
+ if (existsSync10(file)) return readFileSync9(file, "utf8");
9251
9330
  }
9252
9331
  return void 0;
9253
9332
  }
@@ -9487,8 +9566,8 @@ async function main() {
9487
9566
  return 0;
9488
9567
  }
9489
9568
  case "trust": {
9490
- const file = join9(cwd, CONFIG_DIR, "settings.json");
9491
- if (!existsSync9(file)) {
9569
+ const file = join10(cwd, CONFIG_DIR, "settings.json");
9570
+ if (!existsSync10(file)) {
9492
9571
  process.stderr.write(`${file} does not exist; there is nothing to approve
9493
9572
  `);
9494
9573
  return 2;
@@ -9548,9 +9627,9 @@ ${"".padEnd(12)} tools: ${tools}
9548
9627
  for (const [k, v, from] of rows) process.stdout.write(`${k.padEnd(16)} ${v.padEnd(40)} ${from}
9549
9628
  `);
9550
9629
  process.stdout.write(`
9551
- user file ${stored.userPath}${existsSync9(stored.userPath) ? "" : " (absent)"}
9630
+ user file ${stored.userPath}${existsSync10(stored.userPath) ? "" : " (absent)"}
9552
9631
  `);
9553
- const projectState = !existsSync9(stored.projectPath) ? " (absent)" : stored.projectApplied ? " (applied)" : " (present, not trusted \u2014 run `motif trust`)";
9632
+ const projectState = !existsSync10(stored.projectPath) ? " (absent)" : stored.projectApplied ? " (applied)" : " (present, not trusted \u2014 run `motif trust`)";
9554
9633
  process.stdout.write(`project file ${stored.projectPath}${projectState}
9555
9634
  `);
9556
9635
  return 0;
@@ -9566,7 +9645,7 @@ user file ${stored.userPath}${existsSync9(stored.userPath) ? "" : " (absent)
9566
9645
  return worstState(checks) === "fail" ? 1 : 0;
9567
9646
  }
9568
9647
  case "sessions": {
9569
- const dir = join9(cwd, CONFIG_DIR, "sessions");
9648
+ const dir = join10(cwd, CONFIG_DIR, "sessions");
9570
9649
  const sessions = listSessions(dir);
9571
9650
  if (sessions.length === 0) {
9572
9651
  process.stdout.write(`no sessions in ${dir}
@@ -9584,7 +9663,7 @@ user file ${stored.userPath}${existsSync9(stored.userPath) ? "" : " (absent)
9584
9663
  return 0;
9585
9664
  }
9586
9665
  case "distil": {
9587
- const dir = args.rest[0] ?? join9(cwd, CONFIG_DIR, "sessions");
9666
+ const dir = args.rest[0] ?? join10(cwd, CONFIG_DIR, "sessions");
9588
9667
  const format = flagEnum(args.flags, "format", DISTIL_FORMATS, "trajectory-jsonl");
9589
9668
  const filter = flagEnum(args.flags, "filter", DISTIL_FILTERS, "grader-passed");
9590
9669
  const includeChildren = args.flags["include-children"] === true;
@@ -9606,7 +9685,7 @@ user file ${stored.userPath}${existsSync9(stored.userPath) ? "" : " (absent)
9606
9685
  return 0;
9607
9686
  }
9608
9687
  case "metrics": {
9609
- const dir = args.rest[0] ?? join9(cwd, CONFIG_DIR, "sessions");
9688
+ const dir = args.rest[0] ?? join10(cwd, CONFIG_DIR, "sessions");
9610
9689
  for (const s of listSessions(dir)) {
9611
9690
  const parsed = parseJournal(readFile(s.path, "utf8"));
9612
9691
  for (const t of toTrajectories(parsed)) {
@@ -9712,22 +9791,23 @@ user file ${stored.userPath}${existsSync9(stored.userPath) ? "" : " (absent)
9712
9791
  },
9713
9792
  settingsInfo: stored,
9714
9793
  persist: (key, value) => saveUserSetting(key, value),
9715
- historyPath: join9(cwd, CONFIG_DIR, "history.jsonl"),
9794
+ historyPath: join10(cwd, CONFIG_DIR, "history.jsonl"),
9716
9795
  pluginLines: describePlugins(plugins(cwd)),
9717
9796
  hookLines: describeHooks(cwd, hooks),
9718
- notesPath: join9(cwd, CONFIG_DIR, "NOTES.md"),
9719
- ...args.flags["continue"] === true ? { continueFrom: listSessions(join9(cwd, CONFIG_DIR, "sessions"))[0]?.path ?? "" } : {},
9797
+ notesPath: join10(cwd, CONFIG_DIR, "NOTES.md"),
9798
+ ...args.flags["continue"] === true ? { continueFrom: listSessions(join10(cwd, CONFIG_DIR, "sessions"))[0]?.path ?? "" } : {},
9720
9799
  channelPolicy,
9721
9800
  ...apiKey !== void 0 ? { apiKey } : {},
9722
9801
  ...connection.sources.apiKey !== void 0 ? { apiKeySource: connection.sources.apiKey } : {},
9723
9802
  requireKey: keyRequired(endpoint),
9724
9803
  envPath: defaultEnvPath(),
9804
+ offerInstall: ranFromNpx(process.argv[1]) && !commandOnPath("motif"),
9725
9805
  skills,
9726
9806
  agents,
9727
9807
  hooks,
9728
9808
  ...projectNotes !== void 0 ? { projectNotes } : {},
9729
9809
  tools: activeTools,
9730
- journalDir: join9(cwd, CONFIG_DIR, "sessions"),
9810
+ journalDir: join10(cwd, CONFIG_DIR, "sessions"),
9731
9811
  version: VERSION,
9732
9812
  hero: args.flags["no-hero"] !== true,
9733
9813
  ...task ? { initialTask: task } : {}
@@ -9789,7 +9869,7 @@ user file ${stored.userPath}${existsSync9(stored.userPath) ? "" : " (absent)
9789
9869
  const transport = new HttpTransport({ endpoint, model, ...sessionKey !== void 0 ? { apiKey: sessionKey } : {} });
9790
9870
  const screen = new Screen({ showThinking, verbose: args.flags["verbose"] === true, cwd });
9791
9871
  const runId = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9792
- const journalPath = flagStr(args.flags, "journal", "") || join9(cwd, CONFIG_DIR, "sessions", `${runId}.jsonl`);
9872
+ const journalPath = flagStr(args.flags, "journal", "") || join10(cwd, CONFIG_DIR, "sessions", `${runId}.jsonl`);
9793
9873
  const journal = new Journal(
9794
9874
  journalPath,
9795
9875
  newHeader({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "motifcode",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Unofficial coding agent harness built for Motif-3: a Claude Code-style terminal session over the hosted endpoint",
6
6
  "keywords": [