@mmerterden/multi-agent-pipeline 20.30.0 → 20.34.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/CHANGELOG.md +123 -0
- package/README.md +47 -1
- package/README.tr.md +61 -1
- package/docs/adr/0018-cross-repo-links-not-a-global-graph.md +43 -0
- package/docs/adr/README.md +2 -1
- package/docs/facts.json +6 -5
- package/docs/features.md +1 -1
- package/install/_dev-only-files.mjs +1 -0
- package/install/_unattended-profile.mjs +61 -0
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +16 -2
- package/install/index.mjs +10 -1
- package/install/templates/claude-hooks.json +23 -1
- package/manifest.json +198 -134
- package/package.json +4 -1
- package/pipeline/agents/code-reviewer.md +9 -0
- package/pipeline/agents/explorer.md +4 -0
- package/pipeline/agents/plan-critic.md +4 -2
- package/pipeline/commands/multi-agent/SKILL.md +17 -136
- package/pipeline/commands/multi-agent/analysis/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/channels/SKILL.md +15 -297
- package/pipeline/commands/multi-agent/graph/SKILL.md +5 -2
- package/pipeline/commands/multi-agent/language/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/office-on/SKILL.md +8 -7
- package/pipeline/commands/multi-agent/office-status/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +4 -1
- package/pipeline/commands/multi-agent/resume/SKILL.md +8 -22
- package/pipeline/commands/multi-agent/review/SKILL.md +11 -134
- package/pipeline/commands/multi-agent/scenario-audit/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/setup/SKILL.md +19 -224
- package/pipeline/commands/multi-agent/sync/SKILL.md +12 -0
- package/pipeline/commands/multi-agent/test-gen/SKILL.md +29 -3
- package/pipeline/contract/CHANGELOG.md +46 -0
- package/pipeline/contract/README.md +27 -10
- package/pipeline/contract/fixtures/error-invalid-request.json +1 -1
- package/pipeline/contract/fixtures/error-unauthorized.json +1 -1
- package/pipeline/contract/fixtures/error-unsigned.json +1 -1
- package/pipeline/contract/fixtures/launch-plan.json +1 -1
- package/pipeline/contract/fixtures/runs-awaiting-question.json +6 -1
- package/pipeline/contract/fixtures/runs-empty.json +1 -1
- package/pipeline/contract/fixtures/runs-failed.json +6 -1
- package/pipeline/contract/fixtures/runs-old-schema.json +6 -1
- package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +5 -1
- package/pipeline/contract/fixtures/runs-pr-opened.json +6 -1
- package/pipeline/contract/fixtures/runs-running.json +6 -1
- package/pipeline/contract/manifest.json +153 -19
- package/pipeline/contract/types/index.d.ts +304 -6
- package/pipeline/lib/bg-launch.mjs +106 -0
- package/pipeline/lib/code-digest.mjs +49 -0
- package/pipeline/lib/credential-resolve.mjs +42 -2
- package/pipeline/multi-agent-refs/channels/generator.md +146 -0
- package/pipeline/multi-agent-refs/channels/menu.md +55 -0
- package/pipeline/multi-agent-refs/channels/multi-repo.md +6 -0
- package/pipeline/multi-agent-refs/channels/preview.md +25 -0
- package/pipeline/multi-agent-refs/channels/reference.md +98 -0
- package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +1 -1
- package/pipeline/multi-agent-refs/features/code-graph.md +27 -0
- package/pipeline/multi-agent-refs/features/office-threat-model.md +24 -6
- package/pipeline/multi-agent-refs/features/plan-critic.md +5 -2
- package/pipeline/multi-agent-refs/features/review-delta.md +4 -1
- package/pipeline/multi-agent-refs/features/scope-check.md +10 -1
- package/pipeline/multi-agent-refs/features/unattended-security.md +1 -1
- package/pipeline/multi-agent-refs/help/en.md +2 -2
- package/pipeline/multi-agent-refs/help/tr.md +2 -2
- package/pipeline/multi-agent-refs/knowledge.md +9 -0
- package/pipeline/multi-agent-refs/orchestrator/dispatcher.md +163 -0
- package/pipeline/multi-agent-refs/phases/modes.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +4 -4
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +9 -8
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +12 -7
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +7 -3
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +19 -10
- package/pipeline/multi-agent-refs/picker-contract.md +1 -1
- package/pipeline/multi-agent-refs/resume/details.md +35 -0
- package/pipeline/multi-agent-refs/review/inputs.md +87 -0
- package/pipeline/multi-agent-refs/review/module-guides.md +25 -0
- package/pipeline/multi-agent-refs/review/valven.md +42 -0
- package/pipeline/multi-agent-refs/review-standalone-tracker.md +37 -0
- package/pipeline/multi-agent-refs/setup/accounts.md +27 -0
- package/pipeline/multi-agent-refs/setup/app-store-connect.md +5 -0
- package/pipeline/multi-agent-refs/setup/firebase.md +5 -0
- package/pipeline/multi-agent-refs/setup/google-play.md +4 -0
- package/pipeline/multi-agent-refs/setup/hooks-and-plugins.md +21 -0
- package/pipeline/multi-agent-refs/setup/language.md +46 -0
- package/pipeline/multi-agent-refs/setup/multi-repo-group.md +51 -0
- package/pipeline/multi-agent-refs/setup/summaries.md +63 -0
- package/pipeline/multi-agent-refs/setup/token-save-flow.md +51 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +8 -3
- package/pipeline/office/NOTICE.md +4 -1
- package/pipeline/office/assets/ARCHIVE_BOXES.png +0 -0
- package/pipeline/office/assets/COFFEE_MACHINE.png +0 -0
- package/pipeline/office/assets/COUNTER_SINK.png +0 -0
- package/pipeline/office/assets/DEVICE_STAND.png +0 -0
- package/pipeline/office/assets/FILING_CABINET.png +0 -0
- package/pipeline/office/assets/FRIDGE.png +0 -0
- package/pipeline/office/assets/MAILBOX.png +0 -0
- package/pipeline/office/assets/MOP_BUCKET.png +0 -0
- package/pipeline/office/assets/PRINTER.png +0 -0
- package/pipeline/office/assets/RECEPTION_DESK.png +0 -0
- package/pipeline/office/assets/SERVER_RACK.png +0 -0
- package/pipeline/office/assets/SERVER_RACK_2.png +0 -0
- package/pipeline/office/assets/TOILET_STALL.png +0 -0
- package/pipeline/office/assets/VENDING.png +0 -0
- package/pipeline/office/assets/WASHBASIN.png +0 -0
- package/pipeline/office/assets/WATER_COOLER.png +0 -0
- package/pipeline/office/index.html +3936 -819
- package/pipeline/office/toolkit.json +3 -0
- package/pipeline/rules/response-shape.md +6 -7
- package/pipeline/schemas/activity.schema.json +188 -4
- package/pipeline/schemas/agent-state.schema.json +511 -115
- package/pipeline/schemas/contract-error.schema.json +12 -1
- package/pipeline/schemas/job-change.schema.json +72 -0
- package/pipeline/schemas/job-request.schema.json +38 -0
- package/pipeline/schemas/launch.json +4 -4
- package/pipeline/schemas/office-credentials.schema.json +71 -0
- package/pipeline/schemas/office-jobs.json +253 -0
- package/pipeline/schemas/office-jobs.schema.json +62 -0
- package/pipeline/schemas/office-memory.schema.json +105 -0
- package/pipeline/schemas/office-settings-request.schema.json +30 -0
- package/pipeline/schemas/office-settings.schema.json +67 -0
- package/pipeline/schemas/phases.json +14 -6
- package/pipeline/schemas/phases.schema.json +86 -19
- package/pipeline/schemas/plan-critique.schema.json +9 -2
- package/pipeline/schemas/planning-output.schema.json +25 -2
- package/pipeline/schemas/prefs.schema.json +24 -1
- package/pipeline/schemas/prs.schema.json +5 -0
- package/pipeline/schemas/prune-logs-request.schema.json +26 -0
- package/pipeline/schemas/prune-logs.schema.json +52 -0
- package/pipeline/schemas/repo-change.schema.json +54 -0
- package/pipeline/schemas/repo-request.schema.json +29 -0
- package/pipeline/schemas/repos.schema.json +49 -1
- package/pipeline/schemas/reviewer-output.schema.json +20 -2
- package/pipeline/schemas/runs-index.schema.json +257 -46
- package/pipeline/schemas/system-status.schema.json +111 -0
- package/pipeline/schemas/token-budget.json +4 -4
- package/pipeline/schemas/tools.schema.json +5 -0
- package/pipeline/schemas/triage-output.schema.json +20 -2
- package/pipeline/schemas/unattended-profile.json +7 -2
- package/pipeline/scripts/_active-run.mjs +2 -1
- package/pipeline/scripts/_graph-analysis.mjs +270 -0
- package/pipeline/scripts/_memory-policy.mjs +165 -0
- package/pipeline/scripts/_secret-scan.mjs +105 -0
- package/pipeline/scripts/_spawn-async.mjs +9 -5
- package/pipeline/scripts/_unified-diff.mjs +2 -2
- package/pipeline/scripts/activity-snapshot.mjs +219 -23
- package/pipeline/scripts/autopilot-runner.mjs +7 -5
- package/pipeline/scripts/capture-flush.sh +12 -2
- package/pipeline/scripts/check-read-size.sh +9 -0
- package/pipeline/scripts/code-graph-rules/android.json +8 -3
- package/pipeline/scripts/contract-server.mjs +575 -28
- package/pipeline/scripts/debt-ledger.mjs +323 -0
- package/pipeline/scripts/file-context.mjs +261 -0
- package/pipeline/scripts/file-context.sh +20 -0
- package/pipeline/scripts/gen-mode-dispatch.mjs +3 -1
- package/pipeline/scripts/graph-links.mjs +230 -0
- package/pipeline/scripts/graph-query.mjs +133 -1
- package/pipeline/scripts/graph-report.mjs +65 -0
- package/pipeline/scripts/job-launch.mjs +477 -0
- package/pipeline/scripts/launch-request.mjs +94 -9
- package/pipeline/scripts/learn-from-transcripts.mjs +12 -48
- package/pipeline/scripts/learnings-ledger.mjs +143 -24
- package/pipeline/scripts/office-build.mjs +148 -11
- package/pipeline/scripts/office-control.mjs +34 -7
- package/pipeline/scripts/office-credentials.mjs +106 -0
- package/pipeline/scripts/office-memory.mjs +200 -0
- package/pipeline/scripts/office-prune.mjs +125 -0
- package/pipeline/scripts/office-settings.mjs +145 -0
- package/pipeline/scripts/phase-tracker.sh +12 -5
- package/pipeline/scripts/plan-critique-gate.mjs +11 -2
- package/pipeline/scripts/prune-logs.sh +53 -2
- package/pipeline/scripts/rework-rulings.mjs +106 -0
- package/pipeline/scripts/runs-index.mjs +62 -2
- package/pipeline/scripts/scope-check-gate.mjs +149 -2
- package/pipeline/scripts/system-status.mjs +196 -0
- package/pipeline/scripts/task-mirror.mjs +150 -0
- package/pipeline/scripts/task-mirror.sh +20 -0
- package/pipeline/scripts/toolkit-tools.mjs +33 -7
- package/pipeline/scripts/transcript-index.mjs +4 -1
- package/pipeline/scripts/triage-memory.mjs +64 -37
- package/pipeline/scripts/validate-planning.mjs +68 -4
- package/pipeline/scripts/validate-reviewer.mjs +20 -0
- package/pipeline/scripts/validate-triage.mjs +107 -2
- package/pipeline/scripts/work-items.mjs +191 -22
- package/pipeline/skills/.skill-manifest.json +9 -9
- package/pipeline/skills/.skills-index.json +1 -1
- package/pipeline/skills/shared/README.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +5 -2
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +3 -4
- package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +4 -1
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +21 -0
- package/pipeline/skills/shared/core/multi-agent-scenario-audit/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +4 -4
- package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +26 -3
- package/pipeline/skills/skills-index.md +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* on [--no-open] start contract-server.mjs in the background with the
|
|
8
8
|
* installed panel page (../office/index.html, passed as
|
|
9
|
-
* MA_OFFICE_PAGE)
|
|
9
|
+
* MA_OFFICE_PAGE), running until `off`, or reuse the one this script
|
|
10
10
|
* started if it is still alive, then open the browser at
|
|
11
11
|
* /office with a one-time code in the URL fragment,
|
|
12
12
|
* through a 0600 launcher file so the code is never on a
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
* script only reports it; it carries no remote code.
|
|
31
31
|
*
|
|
32
32
|
* State: one 0600 file, ~/.claude/logs/multi-agent/office/office.json, with
|
|
33
|
-
* the pid, port and token file of the server this script started.
|
|
34
|
-
*
|
|
33
|
+
* the pid, port and token file of the server this script started. The server
|
|
34
|
+
* runs until office-off stops it; no daemon, hook or watcher runs beside it.
|
|
35
35
|
*
|
|
36
36
|
* Usage:
|
|
37
37
|
* office-control.mjs on [--no-open] [--json]
|
|
@@ -56,11 +56,16 @@ import {
|
|
|
56
56
|
} from "node:fs";
|
|
57
57
|
import { homedir } from "node:os";
|
|
58
58
|
import { dirname, join } from "node:path";
|
|
59
|
+
|
|
60
|
+
import { codeDigest } from "../lib/code-digest.mjs";
|
|
59
61
|
import { fileURLToPath } from "node:url";
|
|
60
62
|
|
|
61
63
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
62
|
-
|
|
63
|
-
|
|
64
|
+
// The panel runs until office-off: no idle stop and no lifetime, because a
|
|
65
|
+
// person keeps it open beside their work. The server still binds 127.0.0.1
|
|
66
|
+
// only, and every office-on that starts one mints a new token.
|
|
67
|
+
const IDLE_MINUTES = 0;
|
|
68
|
+
const MAX_HOURS = 0;
|
|
64
69
|
/** How far a process's start may sit before its token file's startedAt. */
|
|
65
70
|
const START_SKEW_MS = 120 * 1000;
|
|
66
71
|
|
|
@@ -248,10 +253,23 @@ function openBrowser(target, env) {
|
|
|
248
253
|
return r.status === 0;
|
|
249
254
|
}
|
|
250
255
|
|
|
251
|
-
|
|
256
|
+
const digestOf = (server) => (server ? codeDigest(dirname(server)) : null);
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Start (or reuse) the panel server and open the browser. A running server
|
|
260
|
+
* whose code on disk has changed since it started (an update installed a new
|
|
261
|
+
* pipeline) is stopped and a new one started, so the panel never runs on a
|
|
262
|
+
* server older than its page.
|
|
263
|
+
*/
|
|
252
264
|
export async function officeOn({ env = process.env, open = true } = {}) {
|
|
253
265
|
let s = current(env);
|
|
254
266
|
let reused = true;
|
|
267
|
+
let restarted = false;
|
|
268
|
+
if (s && s.serverDigest && s.serverDigest !== digestOf(serverScript(env))) {
|
|
269
|
+
officeOff({ env });
|
|
270
|
+
s = null;
|
|
271
|
+
restarted = true;
|
|
272
|
+
}
|
|
255
273
|
if (!s) {
|
|
256
274
|
reused = false;
|
|
257
275
|
const server = serverScript(env);
|
|
@@ -289,13 +307,22 @@ export async function officeOn({ env = process.env, open = true } = {}) {
|
|
|
289
307
|
idleMinutes: info.idleMinutes,
|
|
290
308
|
maxHours: info.maxHours ?? null,
|
|
291
309
|
startedAt: new Date().toISOString(),
|
|
310
|
+
serverDigest: digestOf(server),
|
|
292
311
|
});
|
|
293
312
|
s = current(env);
|
|
294
313
|
if (!s) throw new Error("the server stopped right after it started");
|
|
295
314
|
}
|
|
296
315
|
const page = `${s.url}/office`;
|
|
297
316
|
const opened = open ? openBrowser(`${page}#code=${await panelCode(s)}`, env) : false;
|
|
298
|
-
return {
|
|
317
|
+
return {
|
|
318
|
+
running: true,
|
|
319
|
+
reused,
|
|
320
|
+
restarted,
|
|
321
|
+
url: page,
|
|
322
|
+
pid: s.pid,
|
|
323
|
+
idleMinutes: s.idleMinutes,
|
|
324
|
+
opened,
|
|
325
|
+
};
|
|
299
326
|
}
|
|
300
327
|
|
|
301
328
|
/** Stop the server this script started. */
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file office-credentials.mjs - the key cabinet: which services the pipeline
|
|
4
|
+
* holds a credential for and whether each one works, never a value.
|
|
5
|
+
*
|
|
6
|
+
* The answer comes from lib/credential-inventory.sh, the same inventory a run
|
|
7
|
+
* consults before asking the user for data an external system holds. That
|
|
8
|
+
* script probes a credential through the credential store with stdout
|
|
9
|
+
* discarded, so a secret never reaches this process; this file keeps only the
|
|
10
|
+
* logical name, the state, the probe verdict and what the credential is for,
|
|
11
|
+
* each checked against a fixed vocabulary.
|
|
12
|
+
*
|
|
13
|
+
* Renewing a credential stays in the terminal (`/multi-agent:setup`): the
|
|
14
|
+
* secret never passes through the browser.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* office-credentials.mjs [--json] [--probe]
|
|
18
|
+
*
|
|
19
|
+
* Exit: 0 printed, 2 usage.
|
|
20
|
+
*
|
|
21
|
+
* @module pipeline/scripts/office-credentials
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync } from "node:fs";
|
|
25
|
+
import { dirname, join } from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
27
|
+
|
|
28
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
29
|
+
import { spawnAsync } from "./_spawn-async.mjs";
|
|
30
|
+
|
|
31
|
+
export const CONTRACT_VERSION = "1.0.0";
|
|
32
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const STATES = new Set(["present", "mapped-but-missing", "unmapped"]);
|
|
34
|
+
const REACH = new Set([
|
|
35
|
+
"not-probed",
|
|
36
|
+
"reachable",
|
|
37
|
+
"auth-rejected",
|
|
38
|
+
"unreachable",
|
|
39
|
+
"no-host-configured",
|
|
40
|
+
"well-formed",
|
|
41
|
+
"not-probeable",
|
|
42
|
+
"malformed",
|
|
43
|
+
"no-credential",
|
|
44
|
+
"probe-error",
|
|
45
|
+
"server-error",
|
|
46
|
+
"tier-1-ready",
|
|
47
|
+
"tier-2-ready",
|
|
48
|
+
"tier-1-no-grant",
|
|
49
|
+
]);
|
|
50
|
+
const NAME = /^[a-z][a-z0-9_]{0,60}$/;
|
|
51
|
+
|
|
52
|
+
function inventoryPath(env) {
|
|
53
|
+
if (env.MA_CREDENTIAL_INVENTORY) return env.MA_CREDENTIAL_INVENTORY;
|
|
54
|
+
for (const p of [join(HERE, "..", "lib", "credential-inventory.sh")]) if (existsSync(p)) return p;
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function officeCredentials({ probe = false, env = process.env } = {}) {
|
|
59
|
+
const script = inventoryPath(env);
|
|
60
|
+
const base = {
|
|
61
|
+
contractVersion: CONTRACT_VERSION,
|
|
62
|
+
generatedAt: new Date().toISOString(),
|
|
63
|
+
probed: probe,
|
|
64
|
+
};
|
|
65
|
+
if (!script) return { ...base, available: false, credentials: [] };
|
|
66
|
+
const r = await spawnAsync("bash", [script, "--json", ...(probe ? ["--probe"] : [])], {
|
|
67
|
+
env,
|
|
68
|
+
timeoutMs: probe ? 45000 : 10000,
|
|
69
|
+
});
|
|
70
|
+
const doc = (() => {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(r.stdout || "");
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
const rows = Array.isArray(doc?.credentials) ? doc.credentials : [];
|
|
78
|
+
return {
|
|
79
|
+
...base,
|
|
80
|
+
available: Boolean(doc),
|
|
81
|
+
credentials: rows
|
|
82
|
+
.filter((c) => typeof c?.logical === "string" && NAME.test(c.logical))
|
|
83
|
+
.map((c) => ({
|
|
84
|
+
logical: c.logical,
|
|
85
|
+
state: STATES.has(c.state) ? c.state : "mapped-but-missing",
|
|
86
|
+
// A verdict this file does not know is reported as unknown, never as
|
|
87
|
+
// not checked: a probe that ran said something.
|
|
88
|
+
reachability: REACH.has(c.reachability) ? c.reachability : probe ? "unknown" : "not-probed",
|
|
89
|
+
capability: typeof c.capability === "string" ? c.capability.slice(0, 160) : null,
|
|
90
|
+
})),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (invokedDirectly(import.meta.url)) {
|
|
95
|
+
const args = process.argv.slice(2);
|
|
96
|
+
if (args.some((a) => a !== "--json" && a !== "--probe")) {
|
|
97
|
+
console.error("usage: office-credentials.mjs [--json] [--probe]");
|
|
98
|
+
process.exit(2);
|
|
99
|
+
}
|
|
100
|
+
const doc = await officeCredentials({ probe: args.includes("--probe") });
|
|
101
|
+
process.stdout.write(
|
|
102
|
+
args.includes("--json")
|
|
103
|
+
? JSON.stringify(doc) + "\n"
|
|
104
|
+
: doc.credentials.map((c) => `${c.logical}: ${c.state}, ${c.reachability}`).join("\n") + "\n",
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file office-memory.mjs - what the pipeline has learned, per repository.
|
|
4
|
+
*
|
|
5
|
+
* The learning loop writes four stores, and this reads them without changing
|
|
6
|
+
* any:
|
|
7
|
+
*
|
|
8
|
+
* code graph ~/.claude/knowledge/<repo>/code-graph.json (graph-build.mjs)
|
|
9
|
+
* learnings ~/.claude/memory/multi-agent/<repo>/learnings-ledger.jsonl
|
|
10
|
+
* (learnings-ledger.mjs: durable facts, conventions, rejected
|
|
11
|
+
* preferences replayed into later runs)
|
|
12
|
+
* past findings .../triage-corpus.jsonl (triage-memory.mjs: review findings)
|
|
13
|
+
* session notes .../session-notes.jsonl (learn-from-transcripts.mjs)
|
|
14
|
+
*
|
|
15
|
+
* For each repository: when the graph was built and how large it is, how many
|
|
16
|
+
* learnings, findings and session notes there are, the newest learnings'
|
|
17
|
+
* statements (redacted and cut short) and when anything was last written. An
|
|
18
|
+
* empty shelf is reported as empty, so a repository the loop never fed shows.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* office-memory.mjs [--json]
|
|
22
|
+
*
|
|
23
|
+
* MA_MEMORY_ROOT and MA_KNOWLEDGE_ROOT override the two roots. Each store is
|
|
24
|
+
* read asynchronously: lines are counted in chunks and only the last
|
|
25
|
+
* MAX_BYTES are parsed, so a store that keeps growing never blocks the server.
|
|
26
|
+
*
|
|
27
|
+
* Exit: 0 printed, 2 usage.
|
|
28
|
+
*
|
|
29
|
+
* @module pipeline/scripts/office-memory
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { constants, createReadStream, existsSync, readdirSync, statSync } from "node:fs";
|
|
33
|
+
import { open } from "node:fs/promises";
|
|
34
|
+
import { homedir } from "node:os";
|
|
35
|
+
import { join } from "node:path";
|
|
36
|
+
|
|
37
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
38
|
+
import { createRedactor } from "./transcript-index.mjs";
|
|
39
|
+
|
|
40
|
+
export const CONTRACT_VERSION = "1.0.0";
|
|
41
|
+
const MAX_REPOS = 60;
|
|
42
|
+
const RECENT = 5;
|
|
43
|
+
const REPO = /^[A-Za-z0-9._-]{1,100}$/;
|
|
44
|
+
const MAX_BYTES = 1024 * 1024;
|
|
45
|
+
|
|
46
|
+
const roots = (env) => ({
|
|
47
|
+
memory: env.MA_MEMORY_ROOT || join(homedir(), ".claude", "memory", "multi-agent"),
|
|
48
|
+
knowledge: env.MA_KNOWLEDGE_ROOT || join(homedir(), ".claude", "knowledge"),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const dirsOf = (root) => {
|
|
52
|
+
try {
|
|
53
|
+
return readdirSync(root, { withFileTypes: true })
|
|
54
|
+
.filter((d) => d.isDirectory() && REPO.test(d.name))
|
|
55
|
+
.map((d) => d.name);
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// A line that is not a JSON object is skipped like an unreadable one.
|
|
62
|
+
const parse = (line) => {
|
|
63
|
+
try {
|
|
64
|
+
const v = JSON.parse(line);
|
|
65
|
+
return v && typeof v === "object" && !Array.isArray(v) ? [v] : [];
|
|
66
|
+
} catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// A store's lines, the same way for a small file read whole and a large one
|
|
72
|
+
// counted in chunks: every newline, and a last line that has none.
|
|
73
|
+
function linesIn(buf) {
|
|
74
|
+
let n = 0;
|
|
75
|
+
for (let i = buf.indexOf(10); i !== -1; i = buf.indexOf(10, i + 1)) n++;
|
|
76
|
+
return n + (buf.length && buf[buf.length - 1] !== 10 ? 1 : 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Counted through the handle already open, so the count is of the same file.
|
|
80
|
+
async function countLines(fh, size) {
|
|
81
|
+
let n = 0;
|
|
82
|
+
let last = 10;
|
|
83
|
+
const stream = createReadStream(null, { fd: fh.fd, start: 0, end: size - 1, autoClose: false });
|
|
84
|
+
for await (const chunk of stream) {
|
|
85
|
+
for (let i = chunk.indexOf(10); i !== -1; i = chunk.indexOf(10, i + 1)) n++;
|
|
86
|
+
if (chunk.length) last = chunk[chunk.length - 1];
|
|
87
|
+
}
|
|
88
|
+
return n + (size > 0 && last !== 10 ? 1 : 0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A store's line count and its last lines parsed (unreadable lines skipped);
|
|
93
|
+
* { count: 0, tail: [] } for no file. Only the last `maxBytes` are parsed.
|
|
94
|
+
*/
|
|
95
|
+
async function store(path, maxBytes) {
|
|
96
|
+
let fh;
|
|
97
|
+
try {
|
|
98
|
+
// Non-blocking and not through a link: a FIFO or a symlink planted in a
|
|
99
|
+
// store's place is skipped instead of holding a worker thread.
|
|
100
|
+
fh = await open(path, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
101
|
+
const st = await fh.stat();
|
|
102
|
+
if (!st.isFile()) return { count: 0, tail: [] };
|
|
103
|
+
const { size } = st;
|
|
104
|
+
const start = Math.max(0, size - maxBytes);
|
|
105
|
+
const buf = Buffer.alloc(size - start);
|
|
106
|
+
await fh.read(buf, 0, buf.length, start);
|
|
107
|
+
let text = buf.toString("utf8");
|
|
108
|
+
if (start > 0) text = text.slice(text.indexOf("\n") + 1);
|
|
109
|
+
const lines = text.split("\n").filter(Boolean);
|
|
110
|
+
const count = start > 0 ? await countLines(fh, size) : linesIn(buf);
|
|
111
|
+
return { count, tail: lines.flatMap(parse) };
|
|
112
|
+
} catch {
|
|
113
|
+
return { count: 0, tail: [] };
|
|
114
|
+
} finally {
|
|
115
|
+
await fh?.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const ms = (v) => {
|
|
120
|
+
const t = Date.parse(v ?? "");
|
|
121
|
+
return Number.isFinite(t) ? t : null;
|
|
122
|
+
};
|
|
123
|
+
const iso = (t) => (t === null ? null : new Date(t).toISOString());
|
|
124
|
+
|
|
125
|
+
async function shelf(name, r, now, R, maxBytes) {
|
|
126
|
+
const dir = join(r.memory, name);
|
|
127
|
+
const graphPath = join(r.knowledge, name, "code-graph.json");
|
|
128
|
+
let graph = null;
|
|
129
|
+
if (existsSync(graphPath)) {
|
|
130
|
+
const st = statSync(graphPath);
|
|
131
|
+
graph = {
|
|
132
|
+
builtAt: iso(st.mtimeMs),
|
|
133
|
+
ageDays: Math.max(0, Math.floor((now - st.mtimeMs) / 86_400_000)),
|
|
134
|
+
sizeKb: Math.round(st.size / 1024),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const [learnings, findings, notes] = await Promise.all(
|
|
138
|
+
["learnings-ledger.jsonl", "triage-corpus.jsonl", "session-notes.jsonl"].map((f) =>
|
|
139
|
+
store(join(dir, f), maxBytes),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
const times = [...learnings.tail, ...findings.tail, ...notes.tail]
|
|
143
|
+
.map((x) => ms(x.ts))
|
|
144
|
+
.filter((t) => t !== null);
|
|
145
|
+
if (graph) times.push(ms(graph.builtAt));
|
|
146
|
+
return {
|
|
147
|
+
repo: name,
|
|
148
|
+
graph,
|
|
149
|
+
learnings: learnings.count,
|
|
150
|
+
findings: findings.count,
|
|
151
|
+
sessionNotes: notes.count,
|
|
152
|
+
recent: learnings.tail
|
|
153
|
+
.slice(-RECENT)
|
|
154
|
+
.reverse()
|
|
155
|
+
.map((x) => ({
|
|
156
|
+
kind: typeof x.kind === "string" ? x.kind.slice(0, 30) : null,
|
|
157
|
+
statement: R.excerpt(String(x.statement ?? ""), 120) ?? "",
|
|
158
|
+
at: iso(ms(x.ts)),
|
|
159
|
+
})),
|
|
160
|
+
lastWrite: times.length ? iso(Math.max(...times)) : null,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function officeMemory({
|
|
165
|
+
env = process.env,
|
|
166
|
+
now = Date.now(),
|
|
167
|
+
maxBytes = MAX_BYTES,
|
|
168
|
+
} = {}) {
|
|
169
|
+
const r = roots(env);
|
|
170
|
+
// The transcript redactor: credentials by provider rule as well as paths
|
|
171
|
+
// and addresses, the same pass the activity view uses.
|
|
172
|
+
const R = createRedactor();
|
|
173
|
+
const names = [...new Set([...dirsOf(r.memory), ...dirsOf(r.knowledge)])];
|
|
174
|
+
// One shelf that cannot be read is left out; the others still answer.
|
|
175
|
+
const read = await Promise.all(names.map((n) => shelf(n, r, now, R, maxBytes).catch(() => null)));
|
|
176
|
+
const shelves = read
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
.sort((a, b) => (b.lastWrite ?? "").localeCompare(a.lastWrite ?? ""))
|
|
179
|
+
.slice(0, MAX_REPOS);
|
|
180
|
+
return { contractVersion: CONTRACT_VERSION, generatedAt: new Date(now).toISOString(), shelves };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (invokedDirectly(import.meta.url)) {
|
|
184
|
+
const args = process.argv.slice(2);
|
|
185
|
+
if (args.some((a) => a !== "--json")) {
|
|
186
|
+
console.error("usage: office-memory.mjs [--json]");
|
|
187
|
+
process.exit(2);
|
|
188
|
+
}
|
|
189
|
+
const doc = await officeMemory();
|
|
190
|
+
process.stdout.write(
|
|
191
|
+
args.includes("--json")
|
|
192
|
+
? JSON.stringify(doc) + "\n"
|
|
193
|
+
: doc.shelves
|
|
194
|
+
.map(
|
|
195
|
+
(s) =>
|
|
196
|
+
`${s.repo}: graph ${s.graph ? s.graph.ageDays + "d old" : "none"}, ${s.learnings} learnings, ${s.findings} findings, ${s.sessionNotes} notes`,
|
|
197
|
+
)
|
|
198
|
+
.join("\n") + "\n",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file office-prune.mjs - old run logs, cleared from the restroom.
|
|
4
|
+
*
|
|
5
|
+
* A thin wrapper over prune-logs.sh, the same script /multi-agent:prune-logs
|
|
6
|
+
* runs: its dry run is the preview (which task log folders would go and the
|
|
7
|
+
* space freed). On the office's route (pruneApply) the confirmed step lists
|
|
8
|
+
* the folders again, refuses with code "changed" when they differ from the
|
|
9
|
+
* preview, and then prunes only that list (prune-logs.sh --only-from); the
|
|
10
|
+
* CLI's `apply` prunes the age directly, as /multi-agent:prune-logs does.
|
|
11
|
+
* Everything prune-logs.sh guarantees holds here: the audit trail,
|
|
12
|
+
* the metrics corpus and the task counters are never touched, the log root is
|
|
13
|
+
* refused when it is /, the home folder or a git checkout, a folder written
|
|
14
|
+
* in the last minutes is spared so a live run keeps its log, and so is a run
|
|
15
|
+
* that can still be resumed.
|
|
16
|
+
*
|
|
17
|
+
* The age is one of a fixed set of days; nothing else reaches the script.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* office-prune.mjs preview <days> [--json]
|
|
21
|
+
* office-prune.mjs apply <days> [--json]
|
|
22
|
+
*
|
|
23
|
+
* PRUNE_LOG_ROOT overrides the log root, as for prune-logs.sh.
|
|
24
|
+
*
|
|
25
|
+
* Exit: 0 printed, 1 prune-logs.sh refused, 2 usage.
|
|
26
|
+
*
|
|
27
|
+
* @module pipeline/scripts/office-prune
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
31
|
+
import { tmpdir } from "node:os";
|
|
32
|
+
import { dirname, join } from "node:path";
|
|
33
|
+
import { fileURLToPath } from "node:url";
|
|
34
|
+
|
|
35
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
36
|
+
import { spawnAsync } from "./_spawn-async.mjs";
|
|
37
|
+
|
|
38
|
+
export const CONTRACT_VERSION = "1.0.0";
|
|
39
|
+
export const AGES = [7, 30, 90];
|
|
40
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
41
|
+
const MAX_LISTED = 200;
|
|
42
|
+
|
|
43
|
+
/** What a prune-logs.sh run printed: the folders and the space freed. */
|
|
44
|
+
export function parsePrune(text) {
|
|
45
|
+
const lines = String(text).split("\n");
|
|
46
|
+
const head = lines.find((l) => /would remove|removed/i.test(l)) || "";
|
|
47
|
+
const removed = /removed (\d+)\/\d+ task log dir/.exec(head);
|
|
48
|
+
const n = Number(removed ? removed[1] : ((/(\d+) task log dir/.exec(head) || [])[1] ?? 0));
|
|
49
|
+
const size = /~(\d+(?:\.\d+)?) ?(KB|MB)/.exec(head);
|
|
50
|
+
const mb = size
|
|
51
|
+
? size[2] === "KB"
|
|
52
|
+
? Math.round((Number(size[1]) / 1024) * 10) / 10
|
|
53
|
+
: Number(size[1])
|
|
54
|
+
: 0;
|
|
55
|
+
const all = lines
|
|
56
|
+
.map((l) => /^\s{2}(\S(?:.*\S)?)\s*$/.exec(l))
|
|
57
|
+
.filter(Boolean)
|
|
58
|
+
.map((m) => m[1]);
|
|
59
|
+
return { count: n || all.length, freedMb: mb, folders: all.slice(0, MAX_LISTED), all };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The page sees at most MAX_LISTED folders; the confirmation compares the
|
|
63
|
+
// whole list.
|
|
64
|
+
async function run(days, apply, env, { keepAll = false, only = null } = {}) {
|
|
65
|
+
if (!AGES.includes(days)) throw new Error(`age must be one of ${AGES.join(", ")}`);
|
|
66
|
+
const dir = only ? mkdtempSync(join(tmpdir(), "office-prune-")) : null;
|
|
67
|
+
const list = dir ? join(dir, "only.txt") : null;
|
|
68
|
+
if (list) writeFileSync(list, only.join("\n") + "\n", { mode: 0o600 });
|
|
69
|
+
const r = await spawnAsync(
|
|
70
|
+
"bash",
|
|
71
|
+
[
|
|
72
|
+
join(HERE, "prune-logs.sh"),
|
|
73
|
+
`--older-than=${days}`,
|
|
74
|
+
...(list ? [`--only-from=${list}`] : []),
|
|
75
|
+
...(apply ? ["--yes"] : []),
|
|
76
|
+
],
|
|
77
|
+
{ env, timeoutMs: 120_000 },
|
|
78
|
+
).finally(() => dir && rmSync(dir, { recursive: true, force: true }));
|
|
79
|
+
if (r.status !== 0) throw new Error("prune-logs.sh refused (exit " + r.status + ")");
|
|
80
|
+
const { all, ...parsed } = parsePrune(r.stdout);
|
|
81
|
+
return {
|
|
82
|
+
contractVersion: CONTRACT_VERSION,
|
|
83
|
+
olderThanDays: days,
|
|
84
|
+
applied: apply,
|
|
85
|
+
...parsed,
|
|
86
|
+
...(keepAll ? { all } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const prunePreview = (days, env = process.env, opts = {}) => run(days, false, env, opts);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Prunes only what the preview showed: the folders are listed again and the
|
|
94
|
+
* prune runs only when they are the same set, and then only on that list, so
|
|
95
|
+
* a run that aged into the window or a folder that appeared since is never
|
|
96
|
+
* removed unseen.
|
|
97
|
+
*/
|
|
98
|
+
export async function pruneApply(days, env = process.env, expected = null) {
|
|
99
|
+
if (expected && Array.isArray(expected.all)) {
|
|
100
|
+
const now = await run(days, false, env, { keepAll: true });
|
|
101
|
+
const want = new Set(expected.all);
|
|
102
|
+
const same =
|
|
103
|
+
now.count === expected.count &&
|
|
104
|
+
now.all.length === want.size &&
|
|
105
|
+
now.all.every((f) => want.has(f));
|
|
106
|
+
if (!same) {
|
|
107
|
+
const e = new Error("the folders changed since the preview; preview again");
|
|
108
|
+
e.code = "changed";
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
return run(days, true, env, { only: expected.all });
|
|
112
|
+
}
|
|
113
|
+
return run(days, true, env);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (invokedDirectly(import.meta.url)) {
|
|
117
|
+
const [cmd, d, flag] = process.argv.slice(2);
|
|
118
|
+
const days = Number(d);
|
|
119
|
+
if (!["preview", "apply"].includes(cmd) || !AGES.includes(days) || (flag && flag !== "--json")) {
|
|
120
|
+
console.error(`usage: office-prune.mjs preview|apply <${AGES.join("|")}> [--json]`);
|
|
121
|
+
process.exit(2);
|
|
122
|
+
}
|
|
123
|
+
const doc = await run(days, cmd === "apply", process.env);
|
|
124
|
+
process.stdout.write(JSON.stringify(doc) + "\n");
|
|
125
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file office-settings.mjs - the three switches the office can flip, read
|
|
4
|
+
* and changed through the same code the terminal commands use.
|
|
5
|
+
*
|
|
6
|
+
* language prefs.global.outputLanguage, en or tr, as /multi-agent:language
|
|
7
|
+
* writes it (promptLanguage forced back to en in the same write),
|
|
8
|
+
* then localize-commands.mjs swaps the installed picker text
|
|
9
|
+
* model the fable rung, through lib/model-rung.sh on|off
|
|
10
|
+
* route model routing, through lib/route-state.sh on|off; `on` keeps
|
|
11
|
+
* the saved configuration, and anything that would widen it (a
|
|
12
|
+
* free provider, new rules) stays in the terminal
|
|
13
|
+
*
|
|
14
|
+
* Every value is one of a fixed set; nothing else reaches a script.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* office-settings.mjs [--json]
|
|
18
|
+
* office-settings.mjs set <language|model|route> <value> [--json]
|
|
19
|
+
*
|
|
20
|
+
* MULTI_AGENT_PREFS overrides the preferences file, as for the scripts it calls.
|
|
21
|
+
*
|
|
22
|
+
* Exit: 0 printed or applied, 1 the change failed, 2 usage.
|
|
23
|
+
*
|
|
24
|
+
* @module pipeline/scripts/office-settings
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync } from "node:fs";
|
|
28
|
+
import { homedir } from "node:os";
|
|
29
|
+
import { dirname, join } from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
|
|
32
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
33
|
+
import { updateJsonFileSync } from "../lib/json-file-lock.mjs";
|
|
34
|
+
import { readRegularJson } from "../lib/regular-file.mjs";
|
|
35
|
+
import { spawnAsync } from "./_spawn-async.mjs";
|
|
36
|
+
|
|
37
|
+
export const CONTRACT_VERSION = "1.0.0";
|
|
38
|
+
export const CHOICES = {
|
|
39
|
+
language: ["en", "tr"],
|
|
40
|
+
model: ["on", "off"],
|
|
41
|
+
route: ["on", "off"],
|
|
42
|
+
};
|
|
43
|
+
const STRATEGIES = ["manual", "task-fit", "cost-ceiling"];
|
|
44
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
45
|
+
|
|
46
|
+
const prefsPath = (env) =>
|
|
47
|
+
env.MULTI_AGENT_PREFS || join(env.HOME || homedir(), ".claude", "multi-agent-preferences.json");
|
|
48
|
+
|
|
49
|
+
function libScript(name) {
|
|
50
|
+
const p = join(HERE, "..", "lib", name);
|
|
51
|
+
return existsSync(p) ? p : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What the switches are set to now. */
|
|
55
|
+
export function readSettings(env = process.env) {
|
|
56
|
+
const g = readRegularJson(prefsPath(env), {})?.global ?? {};
|
|
57
|
+
return {
|
|
58
|
+
contractVersion: CONTRACT_VERSION,
|
|
59
|
+
language: g.outputLanguage === "tr" ? "tr" : "en",
|
|
60
|
+
model: g.modelFallback?.fableEnabled === true ? "on" : "off",
|
|
61
|
+
route: g.modelRouting?.enabled === true ? "on" : "off",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The free provider chain a saved routing configuration keeps across `route
|
|
67
|
+
* off`, which `route on` brings back; null when none is saved.
|
|
68
|
+
*/
|
|
69
|
+
export function freeChainOf(env = process.env) {
|
|
70
|
+
const free = readRegularJson(prefsPath(env), {})?.global?.modelRouting?.free;
|
|
71
|
+
if (free?.enabled !== true) return null;
|
|
72
|
+
return { enabled: true, allowTraining: free.allowTraining === true };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class SettingsError extends Error {}
|
|
76
|
+
|
|
77
|
+
// route-state.sh on resets the strategy to manual unless told otherwise; the
|
|
78
|
+
// saved one is passed back so `on` changes only the switch.
|
|
79
|
+
function routeArgs(setting, value, env) {
|
|
80
|
+
if (setting !== "route" || value !== "on") return [];
|
|
81
|
+
const saved = readRegularJson(prefsPath(env), {})?.global?.modelRouting?.strategy;
|
|
82
|
+
return [`--strategy=${STRATEGIES.includes(saved) ? saved : "manual"}`];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Flip one switch to one of its values; returns the settings afterwards. */
|
|
86
|
+
export async function applySetting(setting, value, env = process.env) {
|
|
87
|
+
if (!CHOICES[setting]?.includes(value)) throw new SettingsError(`${setting}: not a choice`);
|
|
88
|
+
if (setting === "language") {
|
|
89
|
+
const path = prefsPath(env);
|
|
90
|
+
if (!existsSync(path)) throw new SettingsError("no preferences file");
|
|
91
|
+
let written = false;
|
|
92
|
+
updateJsonFileSync(path, (cur) => {
|
|
93
|
+
if (!cur || typeof cur !== "object" || Array.isArray(cur)) return undefined;
|
|
94
|
+
written = true;
|
|
95
|
+
const global = { ...(cur.global ?? {}), promptLanguage: "en", outputLanguage: value };
|
|
96
|
+
return { ...cur, global };
|
|
97
|
+
});
|
|
98
|
+
if (!written)
|
|
99
|
+
throw new SettingsError("language: the preferences file is not an object, not written");
|
|
100
|
+
const loc = join(HERE, "localize-commands.mjs");
|
|
101
|
+
if (existsSync(loc)) {
|
|
102
|
+
const r = await spawnAsync(process.execPath, [loc, value === "tr" ? "apply" : "restore"], {
|
|
103
|
+
env,
|
|
104
|
+
timeoutMs: 30_000,
|
|
105
|
+
});
|
|
106
|
+
if (r.status !== 0)
|
|
107
|
+
throw new SettingsError(
|
|
108
|
+
`language: saved, but the command descriptions were not switched (exit ${r.status})`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
const script = libScript(setting === "model" ? "model-rung.sh" : "route-state.sh");
|
|
113
|
+
if (!script) throw new SettingsError(`${setting}: script not installed`);
|
|
114
|
+
const r = await spawnAsync("bash", [script, value, ...routeArgs(setting, value, env)], {
|
|
115
|
+
env,
|
|
116
|
+
timeoutMs: 30_000,
|
|
117
|
+
});
|
|
118
|
+
if (r.status !== 0)
|
|
119
|
+
throw new SettingsError(`${setting}: the script refused (exit ${r.status})`);
|
|
120
|
+
}
|
|
121
|
+
return readSettings(env);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (invokedDirectly(import.meta.url)) {
|
|
125
|
+
const args = process.argv.slice(2).filter((a) => a !== "--json");
|
|
126
|
+
const json = process.argv.includes("--json");
|
|
127
|
+
const out = (doc) =>
|
|
128
|
+
process.stdout.write(
|
|
129
|
+
json
|
|
130
|
+
? JSON.stringify(doc) + "\n"
|
|
131
|
+
: `language ${doc.language}, model ${doc.model}, route ${doc.route}\n`,
|
|
132
|
+
);
|
|
133
|
+
if (!args.length) out(readSettings());
|
|
134
|
+
else if (args[0] === "set" && args.length === 3) {
|
|
135
|
+
try {
|
|
136
|
+
out(await applySetting(args[1], args[2]));
|
|
137
|
+
} catch (e) {
|
|
138
|
+
console.error(String(e.message));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
console.error("usage: office-settings.mjs [set <language|model|route> <value>] [--json]");
|
|
143
|
+
process.exit(2);
|
|
144
|
+
}
|
|
145
|
+
}
|