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 +4 -2
- package/dist/build-info.json +2 -2
- package/dist/motif.js +154 -74
- package/package.json +1 -1
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
|
-
|
|
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
|
package/dist/build-info.json
CHANGED
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
6221
|
-
import { dirname as dirname5, join as
|
|
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
|
|
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(
|
|
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
|
|
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 =
|
|
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 ??
|
|
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 =
|
|
8301
|
-
const
|
|
8302
|
-
appendFileSync2(path, `${
|
|
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 =
|
|
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 ??
|
|
8685
|
-
if (!
|
|
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}${
|
|
8748
|
-
const projectState = !
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
8871
|
-
import { join as
|
|
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 =
|
|
8874
|
-
if (!
|
|
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 =
|
|
8887
|
-
if (!
|
|
8965
|
+
const dir = join8(root, "plugins");
|
|
8966
|
+
if (!existsSync8(dir)) return out;
|
|
8888
8967
|
for (const name of readdirSync3(dir).sort()) {
|
|
8889
|
-
const path =
|
|
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 =
|
|
8907
|
-
if (
|
|
8985
|
+
const skillsDir = join8(path, "skills");
|
|
8986
|
+
if (existsSync8(skillsDir)) {
|
|
8908
8987
|
for (const s of readdirSync3(skillsDir).sort()) {
|
|
8909
|
-
const file =
|
|
8910
|
-
if (!
|
|
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 =
|
|
8921
|
-
if (
|
|
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 =
|
|
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(
|
|
8940
|
-
const project = loadPluginsFrom(
|
|
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
|
|
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
|
|
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
|
|
9053
|
+
return join9(home, ".motif", "settings.json");
|
|
8975
9054
|
}
|
|
8976
9055
|
function projectSettingsPath(cwd) {
|
|
8977
|
-
return
|
|
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 (!
|
|
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 (
|
|
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.
|
|
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
|
-
[
|
|
9151
|
-
[
|
|
9229
|
+
[join10(homedir5(), CONFIG_DIR, "skills"), "user"],
|
|
9230
|
+
[join10(cwd, CONFIG_DIR, "skills"), "project"]
|
|
9152
9231
|
]) {
|
|
9153
|
-
if (!
|
|
9232
|
+
if (!existsSync10(dir)) continue;
|
|
9154
9233
|
for (const name of readdirSync4(dir)) {
|
|
9155
|
-
const file =
|
|
9156
|
-
if (!
|
|
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 =
|
|
9169
|
-
if (!
|
|
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
|
-
[
|
|
9208
|
-
[
|
|
9286
|
+
[join10(homedir5(), CONFIG_DIR, "agents"), "user"],
|
|
9287
|
+
[join10(cwd, CONFIG_DIR, "agents"), "project"]
|
|
9209
9288
|
]) {
|
|
9210
|
-
if (!
|
|
9289
|
+
if (!existsSync10(dir)) continue;
|
|
9211
9290
|
for (const name of readdirSync4(dir)) {
|
|
9212
9291
|
if (!name.endsWith(".md")) continue;
|
|
9213
|
-
const file =
|
|
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 =
|
|
9238
|
-
if (
|
|
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",
|
|
9249
|
-
const file =
|
|
9250
|
-
if (
|
|
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 =
|
|
9491
|
-
if (!
|
|
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}${
|
|
9630
|
+
user file ${stored.userPath}${existsSync10(stored.userPath) ? "" : " (absent)"}
|
|
9552
9631
|
`);
|
|
9553
|
-
const projectState = !
|
|
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 =
|
|
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] ??
|
|
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] ??
|
|
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:
|
|
9794
|
+
historyPath: join10(cwd, CONFIG_DIR, "history.jsonl"),
|
|
9716
9795
|
pluginLines: describePlugins(plugins(cwd)),
|
|
9717
9796
|
hookLines: describeHooks(cwd, hooks),
|
|
9718
|
-
notesPath:
|
|
9719
|
-
...args.flags["continue"] === true ? { continueFrom: listSessions(
|
|
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:
|
|
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", "") ||
|
|
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({
|