motifcode 0.2.0 → 0.3.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.
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,
4
- "node": "v24.13.0"
2
+ "version": "0.3.0",
3
+ "bytes": 445102,
4
+ "node": "v22.23.2"
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 = { "{": "}", "[": "]" };
@@ -831,6 +831,32 @@ function looksLikeLeakedToolCall(result) {
831
831
  if (result.unrecoverable.length > 0) return true;
832
832
  return /<tool_call>|<\/tool_call>/.test(result.content);
833
833
  }
834
+ function stripLoneFence(s) {
835
+ const m = /^```(?:json|tool_call)?\s*([\s\S]*?)\s*```$/.exec(s.trim());
836
+ return m ? (m[1] ?? "").trim() : s.trim();
837
+ }
838
+ function recoverBareToolCall(content, ctx) {
839
+ const body = stripLoneFence(content);
840
+ if (!body.startsWith("{") || !body.endsWith("}")) return null;
841
+ const strict = strictLoad(body);
842
+ const outcome = strict !== null ? { value: coerceArgumentsWrapper(strict), info: CLEAN } : repairBlockDetailed(body, ctx);
843
+ if (outcome.value === null) return null;
844
+ const name = typeof outcome.value["name"] === "string" ? outcome.value["name"] : "";
845
+ if (name === "" || !ctx.specs.has(name)) return null;
846
+ return {
847
+ name,
848
+ arguments: asArguments(outcome.value["arguments"]),
849
+ repaired: true,
850
+ repair: { kind: "detag", lossy: outcome.info.lossy, complete: outcome.info.complete }
851
+ };
852
+ }
853
+ function contentLeaksToolCall(content, ctx) {
854
+ if (/<\/?tool_call>/.test(content)) return true;
855
+ for (const m of content.matchAll(/\{[^{}]*"name"\s*:\s*"([a-z_]+)"[\s\S]*?\}/g)) {
856
+ if (ctx.specs.has(m[1])) return true;
857
+ }
858
+ return false;
859
+ }
834
860
 
835
861
  // packages/protocol/src/scrubber.ts
836
862
  var MARKERS = [THINK_OPEN, THINK_CLOSE];
@@ -969,6 +995,24 @@ var ToolCallChannel = class {
969
995
  ...c.repair ? { repair: c.repair } : {}
970
996
  }
971
997
  );
998
+ if (actions.length === 0) {
999
+ const bare = recoverBareToolCall(r.content, ctx);
1000
+ if (bare) {
1001
+ const action = bare.name === "done" ? {
1002
+ kind: "done",
1003
+ summary: String(bare.arguments["summary"] ?? ""),
1004
+ ...typeof bare.arguments["confirm"] === "boolean" ? { confirm: bare.arguments["confirm"] } : {},
1005
+ ...bare.repair ? { repair: bare.repair } : {}
1006
+ } : {
1007
+ kind: "tool",
1008
+ name: bare.name,
1009
+ arguments: bare.arguments,
1010
+ repaired: true,
1011
+ ...bare.repair ? { repair: bare.repair } : {}
1012
+ };
1013
+ return { actions: [action], content: "", unrecoverable: [], truncated: false };
1014
+ }
1015
+ }
972
1016
  return {
973
1017
  actions,
974
1018
  content: r.content.trim(),
@@ -2931,7 +2975,7 @@ async function runLoop(opts) {
2931
2975
  } : { id: nextId(), name: a.name, arguments: a.arguments }
2932
2976
  );
2933
2977
  if (parsed.actions.length === 0) {
2934
- const leaked = parsed.unrecoverable.length > 0 || parsed.truncated || (parsed.invalidArguments?.length ?? 0) > 0 || channel === "toolcall" && looksLikeLeakedToolCall(parseToolCalls(split.content, ctx));
2978
+ const leaked = parsed.unrecoverable.length > 0 || parsed.truncated || (parsed.invalidArguments?.length ?? 0) > 0 || channel === "toolcall" && (looksLikeLeakedToolCall(parseToolCalls(split.content, ctx)) || contentLeaksToolCall(split.content, ctx));
2935
2979
  if (replyEnds && !leaked && response.finishReason !== "length") {
2936
2980
  session.appendAll(codec.serializeAssistant(split.content, split.reasoning, parsed));
2937
2981
  checkpoint();
@@ -2960,7 +3004,8 @@ async function runLoop(opts) {
2960
3004
  leaked ? "Your last turn did not produce a usable action. It looks like action syntax that failed to parse." : "Your last turn produced no action.",
2961
3005
  channel,
2962
3006
  consecutiveNoAction,
2963
- lastObservation
3007
+ lastObservation,
3008
+ !leaked
2964
3009
  );
2965
3010
  if (consecutiveNoAction === 1) {
2966
3011
  handBack(nudge);
@@ -3160,8 +3205,8 @@ function refusalPrompt(refusals, channel) {
3160
3205
  repairPrompt("", channel).trim()
3161
3206
  ].join("\n");
3162
3207
  }
3163
- function repairPrompt(problem, channel, attempt = 1, lastObservation = "") {
3164
- const how = channel === "toolcall" ? "Emit a well-formed `<tool_call>` block. Watch backslashes: inside JSON strings, shell `$` and regex metacharacters must be escaped or avoided." : channel === "object" ? "Reply with a single well-formed JSON object matching the schema you were given." : "Reply with the XML shape you were given. Command bodies are verbatim \u2014 do not escape anything inside them.";
3208
+ function repairPrompt(problem, channel, attempt = 1, lastObservation = "", cleanReply = false) {
3209
+ const how = cleanReply ? "If the task is already complete, or the message only needs an answer, call `done` now with that answer as the summary. Otherwise take the next concrete step toward the task above \u2014 do not read files or run commands looking for unrelated work to do." : channel === "toolcall" ? "Emit a well-formed `<tool_call>` block. Watch backslashes: inside JSON strings, shell `$` and regex metacharacters must be escaped or avoided." : channel === "object" ? "Reply with a single well-formed JSON object matching the schema you were given." : "Reply with the XML shape you were given. Command bodies are verbatim \u2014 do not escape anything inside them.";
3165
3210
  const parts = [problem, "", how];
3166
3211
  if (attempt >= 2) {
3167
3212
  parts.push(
@@ -3698,7 +3743,7 @@ var RULES = [
3698
3743
  // redact commit hashes, base64 test fixtures and half of a lockfile.
3699
3744
  name: "secret-assignment",
3700
3745
  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}`
3746
+ replace: (_m, name, sep3) => `${name}${sep3}${MASK}`
3702
3747
  }
3703
3748
  ];
3704
3749
  function redactText(text) {
@@ -6217,8 +6262,8 @@ var Screen = class {
6217
6262
  };
6218
6263
 
6219
6264
  // 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";
6265
+ import { appendFileSync as appendFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync6, statSync as statSync3 } from "node:fs";
6266
+ import { dirname as dirname5, join as join7, resolve as resolve5 } from "node:path";
6222
6267
 
6223
6268
  // packages/cli/src/files.ts
6224
6269
  import { execFileSync } from "node:child_process";
@@ -6347,6 +6392,24 @@ function expandMentions(text, mentions, opts) {
6347
6392
  ${blocks.join("\n\n")}`, attached };
6348
6393
  }
6349
6394
 
6395
+ // packages/cli/src/install.ts
6396
+ import { existsSync as existsSync5 } from "node:fs";
6397
+ import { delimiter, join as join5, sep as sep2 } from "node:path";
6398
+ function ranFromNpx(entry) {
6399
+ if (!entry) return false;
6400
+ return entry.split(/[\\/]/).includes("_npx");
6401
+ }
6402
+ function commandOnPath(name, pathEnv = process.env["PATH"] ?? "") {
6403
+ for (const dir of pathEnv.split(delimiter)) {
6404
+ if (dir === "" || dir.split(sep2).includes("_npx") || dir.includes(`${sep2}_npx${sep2}`) || dir.includes("/_npx/")) continue;
6405
+ if (existsSync5(join5(dir, name)) || existsSync5(join5(dir, `${name}.cmd`))) return true;
6406
+ }
6407
+ return false;
6408
+ }
6409
+ function installCommand(version) {
6410
+ return `npm install -g motifcode@${version}`;
6411
+ }
6412
+
6350
6413
  // packages/cli/src/login.ts
6351
6414
  import { homedir as homedir3 } from "node:os";
6352
6415
  var KEY_PAGE = "https://infron.ai/dashboard/apiKeys";
@@ -6765,7 +6828,7 @@ async function runSlash(text, ctx) {
6765
6828
  import { execFileSync as execFileSync2 } from "node:child_process";
6766
6829
  import { mkdtempSync, realpathSync as realpathSync2 } from "node:fs";
6767
6830
  import { platform, tmpdir } from "node:os";
6768
- import { join as join5 } from "node:path";
6831
+ import { join as join6 } from "node:path";
6769
6832
  function has(binary) {
6770
6833
  try {
6771
6834
  execFileSync2("/bin/sh", ["-c", `command -v ${binary}`], { stdio: "ignore" });
@@ -6856,7 +6919,7 @@ function detectSandbox() {
6856
6919
  return cached;
6857
6920
  }
6858
6921
  function makeScratch() {
6859
- return mkdtempSync(join5(tmpdir(), "motif-scratch-"));
6922
+ return mkdtempSync(join6(tmpdir(), "motif-scratch-"));
6860
6923
  }
6861
6924
 
6862
6925
  // packages/cli/src/doctor.ts
@@ -6967,22 +7030,23 @@ async function doctor(opts) {
6967
7030
  }
6968
7031
  );
6969
7032
  }
7033
+ const probeBody = JSON.stringify({
7034
+ model: opts.model,
7035
+ temperature: SAMPLING_DEFAULTS.temperature,
7036
+ top_p: SAMPLING_DEFAULTS.top_p,
7037
+ stream: false,
7038
+ max_tokens: 256,
7039
+ messages: [
7040
+ { role: "system", content: "You are a coding agent. Finish by calling the `done` tool." },
7041
+ { role: "user", content: 'Call `done` now with the summary "ok". Do nothing else.' }
7042
+ ],
7043
+ tools: [PROBE_TOOL]
7044
+ });
6970
7045
  try {
6971
7046
  const res = await fetchImpl(`${endpoint}/v1/chat/completions`, {
6972
7047
  method: "POST",
6973
7048
  headers,
6974
- body: JSON.stringify({
6975
- model: opts.model,
6976
- temperature: SAMPLING_DEFAULTS.temperature,
6977
- top_p: SAMPLING_DEFAULTS.top_p,
6978
- stream: false,
6979
- max_tokens: 256,
6980
- messages: [
6981
- { role: "system", content: "You are a coding agent. Finish by calling the `done` tool." },
6982
- { role: "user", content: 'Call `done` now with the summary "ok". Do nothing else.' }
6983
- ],
6984
- tools: [PROBE_TOOL]
6985
- })
7049
+ body: probeBody
6986
7050
  });
6987
7051
  if (res.status === 401 || res.status === 403) {
6988
7052
  const text = (await res.text().catch(() => "")).trim().slice(0, 200);
@@ -7029,12 +7093,31 @@ async function doctor(opts) {
7029
7093
  fix: "the harness splits it client-side; that is a fallback, not the design"
7030
7094
  } : { name: "reasoning parser", state: "unknown", detail: "no reasoning in the probe response" }
7031
7095
  );
7032
- const cached2 = json.usage?.prompt_tokens_details?.cached_tokens;
7096
+ let cached2 = json.usage?.prompt_tokens_details?.cached_tokens;
7097
+ const reports = cached2 !== void 0;
7098
+ try {
7099
+ const again = await fetchImpl(`${endpoint}/v1/chat/completions`, {
7100
+ method: "POST",
7101
+ headers,
7102
+ body: probeBody
7103
+ });
7104
+ if (again.ok) {
7105
+ const json2 = await again.json();
7106
+ const c2 = json2.usage?.prompt_tokens_details?.cached_tokens;
7107
+ if (typeof c2 === "number") cached2 = c2;
7108
+ }
7109
+ } catch {
7110
+ }
7033
7111
  checks.push(
7034
- typeof cached2 === "number" ? {
7112
+ typeof cached2 === "number" && cached2 > 0 ? {
7035
7113
  name: "prefix caching",
7036
7114
  state: "ok",
7037
- detail: `the endpoint reports cached prompt tokens (${cached2} on this probe)`
7115
+ detail: `the endpoint served ${cached2} prompt tokens from its prefix cache on a repeated request`
7116
+ } : reports ? {
7117
+ name: "prefix caching",
7118
+ state: "unknown",
7119
+ detail: "the endpoint reports cached tokens but served none on this probe",
7120
+ fix: "a two-request probe cannot always warm the cache; in a real session the frozen tool order keeps the prefix alive across turns"
7038
7121
  } : {
7039
7122
  name: "prefix caching",
7040
7123
  state: "unknown",
@@ -7096,7 +7179,7 @@ function worstState(checks) {
7096
7179
 
7097
7180
  // packages/cli/src/executor.ts
7098
7181
  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";
7182
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
7100
7183
  import { dirname as dirname4, resolve as resolve4 } from "node:path";
7101
7184
 
7102
7185
  // packages/cli/src/policy.ts
@@ -7359,7 +7442,7 @@ function writeFile(path, content, cwd) {
7359
7442
  try {
7360
7443
  const target = resolve4(cwd, path);
7361
7444
  mkdirSync4(dirname4(target), { recursive: true });
7362
- const existed = existsSync5(target);
7445
+ const existed = existsSync6(target);
7363
7446
  writeFileSync3(target, content, "utf8");
7364
7447
  const lines = content === "" ? 0 : content.split("\n").length;
7365
7448
  return {
@@ -7676,6 +7759,8 @@ var Chat = class {
7676
7759
  shellBusy = false;
7677
7760
  /** A tool call waiting for the person's yes or no, and which of the three answers is selected. */
7678
7761
  pendingConfirm = null;
7762
+ /** A question with numbered answers in place of the prompt, and which one is selected. */
7763
+ pendingChoice = null;
7679
7764
  /** A secret being typed in place of the prompt — the API key at login. */
7680
7765
  pendingSecret = null;
7681
7766
  /** The credential for this session. Starts as the caller's; `/login` replaces it, `/logout` drops it. */
@@ -7752,6 +7837,7 @@ var Chat = class {
7752
7837
  this.screen.append({ kind: "system", title: "login", lines: await this.login("startup") });
7753
7838
  this.refresh();
7754
7839
  }
7840
+ if (this.opts.offerInstall) await this.offerInstall();
7755
7841
  if (this.opts.continueFrom) {
7756
7842
  try {
7757
7843
  this.screen.append({ kind: "system", title: "continuing", lines: await this.resume(this.opts.continueFrom) });
@@ -7770,6 +7856,10 @@ var Chat = class {
7770
7856
  this.answerSecret(key);
7771
7857
  return;
7772
7858
  }
7859
+ if (this.pendingChoice) {
7860
+ this.answerChoice(key);
7861
+ return;
7862
+ }
7773
7863
  if (this.pendingConfirm) {
7774
7864
  this.answerConfirm(key);
7775
7865
  return;
@@ -8030,6 +8120,58 @@ var Chat = class {
8030
8120
  get needsLogin() {
8031
8121
  return this.opts.requireKey === true && this.apiKey === void 0;
8032
8122
  }
8123
+ /** Take over the prompt for one question; resolves with the index chosen, or null on esc. */
8124
+ askChoice(title, lines, options) {
8125
+ return new Promise((resolve6) => {
8126
+ this.pendingChoice = { title, lines, options, selected: 0, resolve: resolve6 };
8127
+ this.refresh();
8128
+ });
8129
+ }
8130
+ /** A number, or ↑↓ and Enter, picks an answer; Esc (and Ctrl-C, Ctrl-D) declines. Nothing else does anything. */
8131
+ answerChoice(key) {
8132
+ const pending = this.pendingChoice;
8133
+ if (!pending) return;
8134
+ const n = pending.options.length;
8135
+ let choice;
8136
+ if (key.type === "enter") choice = pending.selected;
8137
+ else if (key.type === "escape" || key.type === "ctrl" && (key.key === "c" || key.key === "d")) choice = null;
8138
+ else if (key.type === "up") pending.selected = (pending.selected + n - 1) % n;
8139
+ else if (key.type === "down") pending.selected = (pending.selected + 1) % n;
8140
+ else if (key.type === "text" && /^[1-9]$/.test(key.text) && Number(key.text) <= n) choice = Number(key.text) - 1;
8141
+ if (choice !== void 0) {
8142
+ this.pendingChoice = null;
8143
+ pending.resolve(choice);
8144
+ }
8145
+ this.refresh();
8146
+ }
8147
+ /**
8148
+ * `npx motifcode` runs the package without leaving a command behind, and
8149
+ * the first person to try it typed `motif` afterwards and found nothing.
8150
+ * So a run from npx, on a machine without `motif`, is offered the global
8151
+ * install once — npm's own, of exactly the version that is running — and
8152
+ * the session carries on either way.
8153
+ */
8154
+ async offerInstall() {
8155
+ const command = installCommand(this.opts.version);
8156
+ const choice = await this.askChoice(
8157
+ "Install the motif command?",
8158
+ ["This run came from npx, which leaves no command behind.", `${command} puts motif and motifcode on your PATH.`],
8159
+ ["1. Yes, install it now", "2. Not now \u2014 npx motifcode keeps working"]
8160
+ );
8161
+ if (choice !== 0) return;
8162
+ const id = `install-${++this.shellSequence}`;
8163
+ this.screen.append({ kind: "tool", id, name: "bash", args: { command }, repaired: false, hooks: [] });
8164
+ this.refresh();
8165
+ const r = await (this.opts.installGlobal ?? ((c) => runShell(c, { cwd: this.settings.cwd, timeoutMs: 18e4, outputCap: 2e4 })))(command);
8166
+ const ok2 = r.code === 0 && !r.timedOut;
8167
+ const output = r.timedOut ? `${r.output.trim()}
8168
+ (killed after ${Math.round(r.ms / 1e3)}s)`.trim() : r.output.trim() || `(exit ${r.code})`;
8169
+ this.screen.apply({ type: "tool_end", id, ok: ok2, output, ms: r.ms });
8170
+ this.screen.append(
8171
+ 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` }
8172
+ );
8173
+ this.refresh();
8174
+ }
8033
8175
  /** Take over the prompt for one secret; resolves with the text, or null when given up. */
8034
8176
  askSecret(title, lines, prompt, cancelHint) {
8035
8177
  return new Promise((resolve6) => {
@@ -8171,14 +8313,16 @@ var Chat = class {
8171
8313
  draft: this.composer.snapshot(),
8172
8314
  placeholder: PLACEHOLDER,
8173
8315
  ...this.pendingConfirm ? { confirm: this.confirmView(this.pendingConfirm.call, this.pendingConfirm.selected) } : {},
8316
+ ...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
8317
  ...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() ? "@" : "/" } } : {}
8318
+ ...items.length > 0 && !this.pendingConfirm && !this.pendingSecret && !this.pendingChoice ? { menu: { items, selected: clampSelection(this.menuSelected, items.length), prefix: this.mentionOpen() ? "@" : "/" } } : {}
8176
8319
  };
8177
8320
  this.screen.setHint(this.hintText());
8178
8321
  this.screen.setComposer(view);
8179
8322
  }
8180
8323
  hintText() {
8181
8324
  if (this.pendingSecret) return this.pendingSecret.cancelHint;
8325
+ if (this.pendingChoice) return "1 2 or \u2191\u2193 enter \xB7 esc leaves it";
8182
8326
  if (this.pendingConfirm) return "1 2 3 or \u2191\u2193 enter \xB7 esc declines";
8183
8327
  if (this.ctrlCArmedAt > 0 && this.now() - this.ctrlCArmedAt <= CTRL_C_WINDOW_MS) return "ctrl-c again to quit";
8184
8328
  if (this.queued.length > 0) {
@@ -8294,12 +8438,12 @@ ${output.slice(0, 8e3)}
8294
8438
  * is built per task.
8295
8439
  */
8296
8440
  addNote(note) {
8297
- const path = this.opts.notesPath ?? join6(this.settings.cwd, ".motif", "NOTES.md");
8441
+ const path = this.opts.notesPath ?? join7(this.settings.cwd, ".motif", "NOTES.md");
8298
8442
  try {
8299
8443
  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}
8444
+ const existing = existsSync7(path) ? readFileSync6(path, "utf8") : "";
8445
+ const sep3 = existing === "" || existing.endsWith("\n") ? "" : "\n";
8446
+ appendFileSync2(path, `${sep3}- ${note}
8303
8447
  `, "utf8");
8304
8448
  this.projectNotes = readFileSync6(path, "utf8");
8305
8449
  this.screen.append({ kind: "notice", level: "info", text: `noted in ${path}; the next task reads it` });
@@ -8322,7 +8466,7 @@ ${output.slice(0, 8e3)}
8322
8466
  this.tasks.push(task);
8323
8467
  const transport = (this.opts.makeTransport ?? defaultTransport)(this.settings, this.apiKey);
8324
8468
  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`);
8469
+ const journalPath = join7(this.opts.journalDir, `${runId}.jsonl`);
8326
8470
  const scope = { scopeId: "root", scopeKind: "root" };
8327
8471
  const journal = new Journal(
8328
8472
  journalPath,
@@ -8681,8 +8825,8 @@ ${output.slice(0, 8e3)}
8681
8825
  },
8682
8826
  compact: (focus) => this.compactNow(focus),
8683
8827
  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}`];
8828
+ const path = this.opts.notesPath ?? join7(this.settings.cwd, ".motif", "NOTES.md");
8829
+ if (!existsSync7(path)) return [`no notes yet; # <text> at the prompt writes ${path}`];
8686
8830
  return [path, "", ...readFileSync6(path, "utf8").replace(/\s+$/, "").split("\n")];
8687
8831
  },
8688
8832
  hooks: () => this.opts.hookLines ?? ["no hooks"],
@@ -8744,8 +8888,8 @@ ${output.slice(0, 8e3)}
8744
8888
  const lines = rows.map(([k, v, from]) => `${k.padEnd(16)} ${v.padEnd(40)} ${from}`);
8745
8889
  lines.push("");
8746
8890
  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`)";
8891
+ lines.push(`user file ${info.userPath}${existsSync7(info.userPath) ? "" : " (absent)"}`);
8892
+ const projectState = !existsSync7(info.projectPath) ? " (absent)" : info.projectApplied ? " (applied)" : " (present, not trusted \u2014 run `motif trust`)";
8749
8893
  lines.push(`project file ${info.projectPath}${projectState}`);
8750
8894
  }
8751
8895
  lines.push("flags and MOTIF_* in the environment or .env outrank both files; /model and the rest save to the user file");
@@ -8800,7 +8944,7 @@ ${output.slice(0, 8e3)}
8800
8944
  file = picked.path;
8801
8945
  }
8802
8946
  const path = resolve5(this.settings.cwd, file);
8803
- if (!existsSync6(path)) throw new Error(`${path} does not exist`);
8947
+ if (!existsSync7(path)) throw new Error(`${path} does not exist`);
8804
8948
  const state = loadResume(path);
8805
8949
  if (state.corruption !== void 0) throw new Error(`this journal is corrupt (${state.corruption})`);
8806
8950
  if (!state.checkpoint) throw new Error("no checkpoint was written in that session; there is nothing to continue");
@@ -8822,7 +8966,7 @@ ${output.slice(0, 8e3)}
8822
8966
  }
8823
8967
  setCwd(path) {
8824
8968
  const target = resolve5(this.settings.cwd, path);
8825
- if (!existsSync6(target) || !statSync3(target).isDirectory()) return [`not a directory: ${target}`];
8969
+ if (!existsSync7(target) || !statSync3(target).isDirectory()) return [`not a directory: ${target}`];
8826
8970
  if (this.active) return ["a task is running; wait for it or interrupt it first"];
8827
8971
  this.executor.close();
8828
8972
  this.settings.cwd = target;
@@ -8839,7 +8983,7 @@ Input from the person:
8839
8983
  ${input}`;
8840
8984
  }
8841
8985
  function readHistory(path) {
8842
- if (!existsSync6(path)) return [];
8986
+ if (!existsSync7(path)) return [];
8843
8987
  const entries = [];
8844
8988
  for (const line2 of readFileSync6(path, "utf8").split("\n")) {
8845
8989
  if (!line2.trim()) continue;
@@ -8867,11 +9011,11 @@ function defaultTransport(settings, apiKey) {
8867
9011
  }
8868
9012
 
8869
9013
  // 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";
9014
+ import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
9015
+ import { join as join8 } from "node:path";
8872
9016
  function readManifest(dir) {
8873
- const file = join7(dir, "plugin.json");
8874
- if (!existsSync7(file)) return null;
9017
+ const file = join8(dir, "plugin.json");
9018
+ if (!existsSync8(file)) return null;
8875
9019
  const raw = JSON.parse(readFileSync7(file, "utf8"));
8876
9020
  const name = typeof raw["name"] === "string" && raw["name"] !== "" ? raw["name"] : null;
8877
9021
  if (!name) throw new Error(`${file}: plugin.json needs a name`);
@@ -8883,10 +9027,10 @@ function readManifest(dir) {
8883
9027
  }
8884
9028
  function loadPluginsFrom(root, source) {
8885
9029
  const out = { plugins: [], skills: [], agents: [], problems: [] };
8886
- const dir = join7(root, "plugins");
8887
- if (!existsSync7(dir)) return out;
9030
+ const dir = join8(root, "plugins");
9031
+ if (!existsSync8(dir)) return out;
8888
9032
  for (const name of readdirSync3(dir).sort()) {
8889
- const path = join7(dir, name);
9033
+ const path = join8(dir, name);
8890
9034
  let st;
8891
9035
  try {
8892
9036
  st = statSync4(path);
@@ -8903,11 +9047,11 @@ function loadPluginsFrom(root, source) {
8903
9047
  }
8904
9048
  if (!manifest) continue;
8905
9049
  const info = { ...manifest, path, source, skills: [], agents: [] };
8906
- const skillsDir = join7(path, "skills");
8907
- if (existsSync7(skillsDir)) {
9050
+ const skillsDir = join8(path, "skills");
9051
+ if (existsSync8(skillsDir)) {
8908
9052
  for (const s of readdirSync3(skillsDir).sort()) {
8909
- const file = join7(skillsDir, s, "SKILL.md");
8910
- if (!existsSync7(file)) continue;
9053
+ const file = join8(skillsDir, s, "SKILL.md");
9054
+ if (!existsSync8(file)) continue;
8911
9055
  try {
8912
9056
  const skill = parseSkill(readFileSync7(file, "utf8"), source);
8913
9057
  out.skills.push(skill);
@@ -8917,11 +9061,11 @@ function loadPluginsFrom(root, source) {
8917
9061
  }
8918
9062
  }
8919
9063
  }
8920
- const agentsDir = join7(path, "agents");
8921
- if (existsSync7(agentsDir)) {
9064
+ const agentsDir = join8(path, "agents");
9065
+ if (existsSync8(agentsDir)) {
8922
9066
  for (const a of readdirSync3(agentsDir).sort()) {
8923
9067
  if (!a.endsWith(".md")) continue;
8924
- const file = join7(agentsDir, a);
9068
+ const file = join8(agentsDir, a);
8925
9069
  try {
8926
9070
  const agent = parseAgent(readFileSync7(file, "utf8"), source);
8927
9071
  out.agents.push(agent);
@@ -8936,8 +9080,8 @@ function loadPluginsFrom(root, source) {
8936
9080
  return out;
8937
9081
  }
8938
9082
  function loadPlugins(opts) {
8939
- const user = loadPluginsFrom(join7(opts.home, ".motif"), "user");
8940
- const project = loadPluginsFrom(join7(opts.cwd, ".motif"), "project");
9083
+ const user = loadPluginsFrom(join8(opts.home, ".motif"), "user");
9084
+ const project = loadPluginsFrom(join8(opts.cwd, ".motif"), "project");
8941
9085
  return {
8942
9086
  plugins: [...user.plugins, ...project.plugins],
8943
9087
  skills: [...user.skills, ...project.skills],
@@ -8955,9 +9099,9 @@ function describePlugins(loaded) {
8955
9099
  }
8956
9100
 
8957
9101
  // packages/cli/src/settings.ts
8958
- import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "node:fs";
9102
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "node:fs";
8959
9103
  import { homedir as homedir4 } from "node:os";
8960
- import { dirname as dirname6, join as join8 } from "node:path";
9104
+ import { dirname as dirname6, join as join9 } from "node:path";
8961
9105
  var KEYS2 = [
8962
9106
  "model",
8963
9107
  "endpoint",
@@ -8971,10 +9115,10 @@ var KEYS2 = [
8971
9115
  "permissions"
8972
9116
  ];
8973
9117
  function userSettingsPath(home = homedir4()) {
8974
- return join8(home, ".motif", "settings.json");
9118
+ return join9(home, ".motif", "settings.json");
8975
9119
  }
8976
9120
  function projectSettingsPath(cwd) {
8977
- return join8(cwd, ".motif", "settings.json");
9121
+ return join9(cwd, ".motif", "settings.json");
8978
9122
  }
8979
9123
  function parseSettings(text) {
8980
9124
  const values = {};
@@ -9030,7 +9174,7 @@ function loadSettings(opts) {
9030
9174
  const sources = {};
9031
9175
  let projectApplied = false;
9032
9176
  const apply = (path, source, gate) => {
9033
- if (!existsSync8(path)) return false;
9177
+ if (!existsSync9(path)) return false;
9034
9178
  const content = readFileSync8(path, "utf8");
9035
9179
  if (gate && !gate(content)) return false;
9036
9180
  const { values: found, problems } = parseSettings(content);
@@ -9050,7 +9194,7 @@ function loadSettings(opts) {
9050
9194
  function saveUserSetting(key, value, home = homedir4()) {
9051
9195
  const path = userSettingsPath(home);
9052
9196
  let current = {};
9053
- if (existsSync8(path)) {
9197
+ if (existsSync9(path)) {
9054
9198
  try {
9055
9199
  const parsed = JSON.parse(readFileSync8(path, "utf8"));
9056
9200
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) current = parsed;
@@ -9065,7 +9209,7 @@ function saveUserSetting(key, value, home = homedir4()) {
9065
9209
  }
9066
9210
 
9067
9211
  // packages/cli/src/main.ts
9068
- var VERSION = "0.2.0";
9212
+ var VERSION = "0.3.0";
9069
9213
  var SHORT_FLAGS = { p: "print", c: "continue", i: "interactive", v: "verbose", h: "help" };
9070
9214
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
9071
9215
  "print",
@@ -9147,13 +9291,13 @@ function loadSkills(cwd) {
9147
9291
  reg.registerAll(BUILTIN_SKILLS);
9148
9292
  reg.registerAll(plugins(cwd).skills);
9149
9293
  for (const [dir, source] of [
9150
- [join9(homedir5(), CONFIG_DIR, "skills"), "user"],
9151
- [join9(cwd, CONFIG_DIR, "skills"), "project"]
9294
+ [join10(homedir5(), CONFIG_DIR, "skills"), "user"],
9295
+ [join10(cwd, CONFIG_DIR, "skills"), "project"]
9152
9296
  ]) {
9153
- if (!existsSync9(dir)) continue;
9297
+ if (!existsSync10(dir)) continue;
9154
9298
  for (const name of readdirSync4(dir)) {
9155
- const file = join9(dir, name, "SKILL.md");
9156
- if (!existsSync9(file)) continue;
9299
+ const file = join10(dir, name, "SKILL.md");
9300
+ if (!existsSync10(file)) continue;
9157
9301
  try {
9158
9302
  reg.register(parseSkill(readFileSync9(file, "utf8"), source));
9159
9303
  } catch (err) {
@@ -9165,8 +9309,8 @@ function loadSkills(cwd) {
9165
9309
  return reg;
9166
9310
  }
9167
9311
  function loadHooks(cwd, opts) {
9168
- const file = join9(cwd, CONFIG_DIR, "settings.json");
9169
- if (!existsSync9(file)) return DEFAULT_HOOKS;
9312
+ const file = join10(cwd, CONFIG_DIR, "settings.json");
9313
+ if (!existsSync10(file)) return DEFAULT_HOOKS;
9170
9314
  const content = readFileSync9(file, "utf8");
9171
9315
  let parsed;
9172
9316
  try {
@@ -9204,13 +9348,13 @@ function loadAgents(cwd) {
9204
9348
  reg.registerAll(BUILTIN_AGENTS);
9205
9349
  reg.registerAll(plugins(cwd).agents);
9206
9350
  for (const [dir, source] of [
9207
- [join9(homedir5(), CONFIG_DIR, "agents"), "user"],
9208
- [join9(cwd, CONFIG_DIR, "agents"), "project"]
9351
+ [join10(homedir5(), CONFIG_DIR, "agents"), "user"],
9352
+ [join10(cwd, CONFIG_DIR, "agents"), "project"]
9209
9353
  ]) {
9210
- if (!existsSync9(dir)) continue;
9354
+ if (!existsSync10(dir)) continue;
9211
9355
  for (const name of readdirSync4(dir)) {
9212
9356
  if (!name.endsWith(".md")) continue;
9213
- const file = join9(dir, name);
9357
+ const file = join10(dir, name);
9214
9358
  try {
9215
9359
  reg.register(parseAgent(readFileSync9(file, "utf8"), source));
9216
9360
  } catch (err) {
@@ -9234,8 +9378,8 @@ function describeHooks(cwd, hooks) {
9234
9378
  for (const d of defs ?? []) lines.push(`${event.padEnd(12)} ${d.matcher ? `[${d.matcher}] ` : ""}${d.command}${d.blocking ? " (blocking)" : ""}`);
9235
9379
  }
9236
9380
  if (lines.length === 0) lines.push("no hooks configured");
9237
- const file = join9(cwd, CONFIG_DIR, "settings.json");
9238
- if (existsSync9(file)) {
9381
+ const file = join10(cwd, CONFIG_DIR, "settings.json");
9382
+ if (existsSync10(file)) {
9239
9383
  const content = readFileSync9(file, "utf8");
9240
9384
  const trusted = checkTrust(loadTrustStore(), cwd, content).trusted;
9241
9385
  lines.push("", `${file}: ${trusted ? "trusted" : "present but not trusted \u2014 its hooks and settings are not applied; run motif trust"}`);
@@ -9245,9 +9389,9 @@ function describeHooks(cwd, hooks) {
9245
9389
  return lines;
9246
9390
  }
9247
9391
  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");
9392
+ for (const name of ["AGENTS.md", "CLAUDE.md", join10(CONFIG_DIR, "NOTES.md")]) {
9393
+ const file = join10(cwd, name);
9394
+ if (existsSync10(file)) return readFileSync9(file, "utf8");
9251
9395
  }
9252
9396
  return void 0;
9253
9397
  }
@@ -9487,8 +9631,8 @@ async function main() {
9487
9631
  return 0;
9488
9632
  }
9489
9633
  case "trust": {
9490
- const file = join9(cwd, CONFIG_DIR, "settings.json");
9491
- if (!existsSync9(file)) {
9634
+ const file = join10(cwd, CONFIG_DIR, "settings.json");
9635
+ if (!existsSync10(file)) {
9492
9636
  process.stderr.write(`${file} does not exist; there is nothing to approve
9493
9637
  `);
9494
9638
  return 2;
@@ -9548,9 +9692,9 @@ ${"".padEnd(12)} tools: ${tools}
9548
9692
  for (const [k, v, from] of rows) process.stdout.write(`${k.padEnd(16)} ${v.padEnd(40)} ${from}
9549
9693
  `);
9550
9694
  process.stdout.write(`
9551
- user file ${stored.userPath}${existsSync9(stored.userPath) ? "" : " (absent)"}
9695
+ user file ${stored.userPath}${existsSync10(stored.userPath) ? "" : " (absent)"}
9552
9696
  `);
9553
- const projectState = !existsSync9(stored.projectPath) ? " (absent)" : stored.projectApplied ? " (applied)" : " (present, not trusted \u2014 run `motif trust`)";
9697
+ const projectState = !existsSync10(stored.projectPath) ? " (absent)" : stored.projectApplied ? " (applied)" : " (present, not trusted \u2014 run `motif trust`)";
9554
9698
  process.stdout.write(`project file ${stored.projectPath}${projectState}
9555
9699
  `);
9556
9700
  return 0;
@@ -9566,7 +9710,7 @@ user file ${stored.userPath}${existsSync9(stored.userPath) ? "" : " (absent)
9566
9710
  return worstState(checks) === "fail" ? 1 : 0;
9567
9711
  }
9568
9712
  case "sessions": {
9569
- const dir = join9(cwd, CONFIG_DIR, "sessions");
9713
+ const dir = join10(cwd, CONFIG_DIR, "sessions");
9570
9714
  const sessions = listSessions(dir);
9571
9715
  if (sessions.length === 0) {
9572
9716
  process.stdout.write(`no sessions in ${dir}
@@ -9584,7 +9728,7 @@ user file ${stored.userPath}${existsSync9(stored.userPath) ? "" : " (absent)
9584
9728
  return 0;
9585
9729
  }
9586
9730
  case "distil": {
9587
- const dir = args.rest[0] ?? join9(cwd, CONFIG_DIR, "sessions");
9731
+ const dir = args.rest[0] ?? join10(cwd, CONFIG_DIR, "sessions");
9588
9732
  const format = flagEnum(args.flags, "format", DISTIL_FORMATS, "trajectory-jsonl");
9589
9733
  const filter = flagEnum(args.flags, "filter", DISTIL_FILTERS, "grader-passed");
9590
9734
  const includeChildren = args.flags["include-children"] === true;
@@ -9606,7 +9750,7 @@ user file ${stored.userPath}${existsSync9(stored.userPath) ? "" : " (absent)
9606
9750
  return 0;
9607
9751
  }
9608
9752
  case "metrics": {
9609
- const dir = args.rest[0] ?? join9(cwd, CONFIG_DIR, "sessions");
9753
+ const dir = args.rest[0] ?? join10(cwd, CONFIG_DIR, "sessions");
9610
9754
  for (const s of listSessions(dir)) {
9611
9755
  const parsed = parseJournal(readFile(s.path, "utf8"));
9612
9756
  for (const t of toTrajectories(parsed)) {
@@ -9712,22 +9856,23 @@ user file ${stored.userPath}${existsSync9(stored.userPath) ? "" : " (absent)
9712
9856
  },
9713
9857
  settingsInfo: stored,
9714
9858
  persist: (key, value) => saveUserSetting(key, value),
9715
- historyPath: join9(cwd, CONFIG_DIR, "history.jsonl"),
9859
+ historyPath: join10(cwd, CONFIG_DIR, "history.jsonl"),
9716
9860
  pluginLines: describePlugins(plugins(cwd)),
9717
9861
  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 ?? "" } : {},
9862
+ notesPath: join10(cwd, CONFIG_DIR, "NOTES.md"),
9863
+ ...args.flags["continue"] === true ? { continueFrom: listSessions(join10(cwd, CONFIG_DIR, "sessions"))[0]?.path ?? "" } : {},
9720
9864
  channelPolicy,
9721
9865
  ...apiKey !== void 0 ? { apiKey } : {},
9722
9866
  ...connection.sources.apiKey !== void 0 ? { apiKeySource: connection.sources.apiKey } : {},
9723
9867
  requireKey: keyRequired(endpoint),
9724
9868
  envPath: defaultEnvPath(),
9869
+ offerInstall: ranFromNpx(process.argv[1]) && !commandOnPath("motif"),
9725
9870
  skills,
9726
9871
  agents,
9727
9872
  hooks,
9728
9873
  ...projectNotes !== void 0 ? { projectNotes } : {},
9729
9874
  tools: activeTools,
9730
- journalDir: join9(cwd, CONFIG_DIR, "sessions"),
9875
+ journalDir: join10(cwd, CONFIG_DIR, "sessions"),
9731
9876
  version: VERSION,
9732
9877
  hero: args.flags["no-hero"] !== true,
9733
9878
  ...task ? { initialTask: task } : {}
@@ -9789,7 +9934,7 @@ user file ${stored.userPath}${existsSync9(stored.userPath) ? "" : " (absent)
9789
9934
  const transport = new HttpTransport({ endpoint, model, ...sessionKey !== void 0 ? { apiKey: sessionKey } : {} });
9790
9935
  const screen = new Screen({ showThinking, verbose: args.flags["verbose"] === true, cwd });
9791
9936
  const runId = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9792
- const journalPath = flagStr(args.flags, "journal", "") || join9(cwd, CONFIG_DIR, "sessions", `${runId}.jsonl`);
9937
+ const journalPath = flagStr(args.flags, "journal", "") || join10(cwd, CONFIG_DIR, "sessions", `${runId}.jsonl`);
9793
9938
  const journal = new Journal(
9794
9939
  journalPath,
9795
9940
  newHeader({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "motifcode",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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": [