@mmerterden/multi-agent-pipeline 20.28.0 → 20.30.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 +19 -0
- package/README.md +19 -6
- package/README.tr.md +5 -5
- package/docs/architecture.md +3 -3
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +7 -7
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +122 -1
- package/install/codex.mjs +2 -0
- package/install/copilot.mjs +13 -0
- package/install/templates/claude-hooks.json +30 -1
- package/manifest.json +189 -77
- package/package.json +2 -1
- package/pipeline/commands/multi-agent/analysis/SKILL.md +3 -1
- package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +13 -15
- package/pipeline/commands/multi-agent/office-off/SKILL.md +27 -0
- package/pipeline/commands/multi-agent/office-on/SKILL.md +50 -0
- package/pipeline/commands/multi-agent/office-status/SKILL.md +29 -0
- package/pipeline/commands/multi-agent/resume/SKILL.md +2 -1
- package/pipeline/commands/multi-agent/serve/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/stack/SKILL.md +15 -8
- package/pipeline/commands/multi-agent/steer/SKILL.md +10 -9
- package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
- package/pipeline/contract/CHANGELOG.md +62 -0
- package/pipeline/contract/README.md +52 -6
- 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/manifest.json +367 -22
- package/pipeline/contract/types/index.d.ts +531 -4
- package/pipeline/lib/hook-registration.mjs +252 -0
- package/pipeline/lib/prefs-value.mjs +44 -0
- package/pipeline/lib/run-completion.mjs +148 -0
- package/pipeline/multi-agent-refs/analysis/render.md +4 -4
- package/pipeline/multi-agent-refs/cross-cli-contract.md +5 -4
- package/pipeline/multi-agent-refs/features/doctor.md +11 -0
- package/pipeline/multi-agent-refs/features/office-threat-model.md +91 -0
- package/pipeline/multi-agent-refs/help/en.md +3 -0
- package/pipeline/multi-agent-refs/help/tr.md +3 -0
- package/pipeline/multi-agent-refs/phases/operations.md +17 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +6 -2
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +7 -6
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +5 -4
- package/pipeline/office/NOTICE.md +42 -0
- package/pipeline/office/assets/BIN.png +0 -0
- package/pipeline/office/assets/BOOKSHELF.png +0 -0
- package/pipeline/office/assets/CACTUS.png +0 -0
- package/pipeline/office/assets/CLOCK.png +0 -0
- package/pipeline/office/assets/COFFEE.png +0 -0
- package/pipeline/office/assets/COFFEE_TABLE.png +0 -0
- package/pipeline/office/assets/CUSHIONED_BENCH.png +0 -0
- package/pipeline/office/assets/CUSHIONED_CHAIR_BACK.png +0 -0
- package/pipeline/office/assets/CUSHIONED_CHAIR_FRONT.png +0 -0
- package/pipeline/office/assets/CUSHIONED_CHAIR_SIDE.png +0 -0
- package/pipeline/office/assets/DESK_FRONT.png +0 -0
- package/pipeline/office/assets/DESK_SIDE.png +0 -0
- package/pipeline/office/assets/DOUBLE_BOOKSHELF.png +0 -0
- package/pipeline/office/assets/HANGING_PLANT.png +0 -0
- package/pipeline/office/assets/LARGE_PAINTING.png +0 -0
- package/pipeline/office/assets/LARGE_PLANT.png +0 -0
- package/pipeline/office/assets/PC_BACK.png +0 -0
- package/pipeline/office/assets/PC_FRONT_OFF.png +0 -0
- package/pipeline/office/assets/PC_FRONT_ON_1.png +0 -0
- package/pipeline/office/assets/PC_FRONT_ON_2.png +0 -0
- package/pipeline/office/assets/PC_FRONT_ON_3.png +0 -0
- package/pipeline/office/assets/PC_SIDE.png +0 -0
- package/pipeline/office/assets/PLANT.png +0 -0
- package/pipeline/office/assets/PLANT_2.png +0 -0
- package/pipeline/office/assets/POT.png +0 -0
- package/pipeline/office/assets/SMALL_PAINTING.png +0 -0
- package/pipeline/office/assets/SMALL_PAINTING_2.png +0 -0
- package/pipeline/office/assets/SMALL_TABLE_FRONT.png +0 -0
- package/pipeline/office/assets/SMALL_TABLE_SIDE.png +0 -0
- package/pipeline/office/assets/SOFA_BACK.png +0 -0
- package/pipeline/office/assets/SOFA_FRONT.png +0 -0
- package/pipeline/office/assets/SOFA_SIDE.png +0 -0
- package/pipeline/office/assets/TABLE_FRONT.png +0 -0
- package/pipeline/office/assets/WHITEBOARD.png +0 -0
- package/pipeline/office/assets/WOODEN_BENCH.png +0 -0
- package/pipeline/office/assets/WOODEN_CHAIR_BACK.png +0 -0
- package/pipeline/office/assets/WOODEN_CHAIR_FRONT.png +0 -0
- package/pipeline/office/assets/WOODEN_CHAIR_SIDE.png +0 -0
- package/pipeline/office/assets/char_0.png +0 -0
- package/pipeline/office/assets/char_1.png +0 -0
- package/pipeline/office/assets/char_2.png +0 -0
- package/pipeline/office/assets/char_3.png +0 -0
- package/pipeline/office/assets/char_4.png +0 -0
- package/pipeline/office/assets/char_5.png +0 -0
- package/pipeline/office/assets/floor_0.png +0 -0
- package/pipeline/office/assets/floor_1.png +0 -0
- package/pipeline/office/assets/floor_2.png +0 -0
- package/pipeline/office/assets/floor_3.png +0 -0
- package/pipeline/office/assets/floor_4.png +0 -0
- package/pipeline/office/assets/floor_5.png +0 -0
- package/pipeline/office/assets/floor_6.png +0 -0
- package/pipeline/office/assets/floor_7.png +0 -0
- package/pipeline/office/assets/floor_8.png +0 -0
- package/pipeline/office/assets/wall_0.png +0 -0
- package/pipeline/office/index.html +5319 -0
- package/pipeline/preferences-template.json +7 -0
- package/pipeline/rules/figma-pipeline.md +2 -0
- package/pipeline/schemas/activity-chat.schema.json +78 -0
- package/pipeline/schemas/activity-roles.json +48 -0
- package/pipeline/schemas/activity-roles.schema.json +49 -0
- package/pipeline/schemas/activity.schema.json +429 -0
- package/pipeline/schemas/agent-state.schema.json +20 -5
- package/pipeline/schemas/autopilot-on-request.schema.json +33 -0
- package/pipeline/schemas/autopilot-on.schema.json +111 -0
- package/pipeline/schemas/contract-error.schema.json +33 -3
- package/pipeline/schemas/issue-maturity.schema.json +68 -0
- package/pipeline/schemas/ledger.schema.json +202 -0
- package/pipeline/schemas/permissions.schema.json +47 -0
- package/pipeline/schemas/phases.json +3 -2
- package/pipeline/schemas/phases.schema.json +5 -1
- package/pipeline/schemas/prefs.schema.json +56 -0
- package/pipeline/schemas/prs.schema.json +73 -0
- package/pipeline/schemas/review-verdict-request.schema.json +20 -0
- package/pipeline/schemas/review-verdict.schema.json +62 -0
- package/pipeline/schemas/routine-delete.schema.json +59 -0
- package/pipeline/schemas/routines.schema.json +38 -0
- package/pipeline/schemas/run-evidence.schema.json +29 -0
- package/pipeline/schemas/run-results.schema.json +218 -0
- package/pipeline/schemas/runs-index.schema.json +1 -1
- package/pipeline/schemas/service-read.schema.json +58 -0
- package/pipeline/schemas/stack-change.schema.json +82 -0
- package/pipeline/schemas/stack-request.schema.json +33 -0
- package/pipeline/schemas/stack.schema.json +59 -0
- package/pipeline/schemas/steer-request.schema.json +25 -0
- package/pipeline/schemas/steer.schema.json +59 -0
- package/pipeline/schemas/tool-call-request.schema.json +19 -0
- package/pipeline/schemas/tool-call.schema.json +47 -0
- package/pipeline/schemas/tools.schema.json +60 -0
- package/pipeline/scripts/_active-run.mjs +158 -0
- package/pipeline/scripts/_autopilot-outcomes.mjs +1 -0
- package/pipeline/scripts/_cost.mjs +66 -1
- package/pipeline/scripts/_guard_rules.py +605 -0
- package/pipeline/scripts/_office-smoke-lib.sh +115 -0
- package/pipeline/scripts/_spawn-async.mjs +73 -0
- package/pipeline/scripts/_transcript-tail.mjs +274 -0
- package/pipeline/scripts/activity-snapshot.mjs +1027 -0
- package/pipeline/scripts/agent-guard.py +118 -7
- package/pipeline/scripts/agent-guard.sh +50 -3
- package/pipeline/scripts/argv-secret-scan.mjs +114 -0
- package/pipeline/scripts/autopilot-control.mjs +214 -9
- package/pipeline/scripts/autopilot-runner.mjs +8 -2
- package/pipeline/scripts/confirm-command.mjs +70 -0
- package/pipeline/scripts/contract-server.mjs +1178 -48
- package/pipeline/scripts/cost-analyze.mjs +2 -53
- package/pipeline/scripts/cost-table.json +1 -0
- package/pipeline/scripts/doctor.mjs +54 -0
- package/pipeline/scripts/evidence-gate.mjs +8 -4
- package/pipeline/scripts/gate-ledger.mjs +163 -7
- package/pipeline/scripts/office-build.mjs +89 -0
- package/pipeline/scripts/office-control.mjs +383 -0
- package/pipeline/scripts/office-ledger.mjs +409 -0
- package/pipeline/scripts/plan-coverage-gate.mjs +9 -8
- package/pipeline/scripts/pre-commit-check.sh +12 -0
- package/pipeline/scripts/routine-registry.mjs +42 -6
- package/pipeline/scripts/run-results.mjs +373 -0
- package/pipeline/scripts/run-scope-guard.mjs +188 -0
- package/pipeline/scripts/run-scope-guard.sh +23 -0
- package/pipeline/scripts/run-stop-guard.mjs +246 -0
- package/pipeline/scripts/run-stop-guard.sh +25 -0
- package/pipeline/scripts/service-read.mjs +244 -0
- package/pipeline/scripts/stack-toggle.mjs +285 -0
- package/pipeline/scripts/steer-run.mjs +97 -0
- package/pipeline/scripts/toolkit-tools.mjs +599 -0
- package/pipeline/scripts/uninstall.mjs +4 -0
- package/pipeline/scripts/validate-state.mjs +26 -0
- package/pipeline/scripts/work-items.mjs +304 -0
- package/pipeline/scripts/write-state.mjs +114 -0
- package/pipeline/skills/.skill-manifest.json +19 -7
- package/pipeline/skills/.skills-index.json +34 -1
- package/pipeline/skills/shared/README.md +6 -3
- package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +13 -15
- package/pipeline/skills/shared/core/multi-agent-office-off/SKILL.md +16 -0
- package/pipeline/skills/shared/core/multi-agent-office-on/SKILL.md +22 -0
- package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +17 -0
- package/pipeline/skills/shared/core/multi-agent-serve/SKILL.md +4 -2
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +14 -6
- package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +10 -9
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- package/pipeline/skills/skills-index.md +4 -1
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file office-control.mjs - start, stop and report the office panel: the
|
|
4
|
+
* one script behind /multi-agent:office-on, office-off and office-status, so
|
|
5
|
+
* the three commands cannot disagree about what is running.
|
|
6
|
+
*
|
|
7
|
+
* on [--no-open] start contract-server.mjs in the background with the
|
|
8
|
+
* installed panel page (../office/index.html, passed as
|
|
9
|
+
* MA_OFFICE_PAGE) and an 8 hour lifetime, or reuse the one this script
|
|
10
|
+
* started if it is still alive, then open the browser at
|
|
11
|
+
* /office with a one-time code in the URL fragment,
|
|
12
|
+
* through a 0600 launcher file so the code is never on a
|
|
13
|
+
* command line. The
|
|
14
|
+
* page trades the code for the token on its first load;
|
|
15
|
+
* the code is valid for 10 seconds and one use. The token
|
|
16
|
+
* is never printed and never put in a URL.
|
|
17
|
+
* off stop the server this script started and forget it. The
|
|
18
|
+
* SIGTERM goes only to a pid whose token file still names
|
|
19
|
+
* it, whose command is contract-server.mjs and whose start
|
|
20
|
+
* time matches the token file's, so a reused pid is never
|
|
21
|
+
* signalled. Nothing is left running: no relay, no session,
|
|
22
|
+
* no watcher exists to stop.
|
|
23
|
+
* status running or not, the URL without the token, the idle
|
|
24
|
+
* limit, remote access, the paired devices the phone
|
|
25
|
+
* registry lists, and the last panel actions from
|
|
26
|
+
* office-actions.jsonl. Remote access is "not available in
|
|
27
|
+
* this installation" unless a remote layer is present: a
|
|
28
|
+
* prefs.global.office.remote block, or a separately
|
|
29
|
+
* installed module at ~/.claude/office-remote/. This
|
|
30
|
+
* script only reports it; it carries no remote code.
|
|
31
|
+
*
|
|
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. Nothing runs
|
|
34
|
+
* while the panel is closed: the server exits by itself after its idle time.
|
|
35
|
+
*
|
|
36
|
+
* Usage:
|
|
37
|
+
* office-control.mjs on [--no-open] [--json]
|
|
38
|
+
* office-control.mjs off [--json]
|
|
39
|
+
* office-control.mjs status [--json]
|
|
40
|
+
*
|
|
41
|
+
* Exit: 0 done, 1 could not start, 2 usage.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
45
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
46
|
+
import {
|
|
47
|
+
chmodSync,
|
|
48
|
+
existsSync,
|
|
49
|
+
mkdirSync,
|
|
50
|
+
openSync,
|
|
51
|
+
readFileSync,
|
|
52
|
+
readdirSync,
|
|
53
|
+
rmSync,
|
|
54
|
+
statSync,
|
|
55
|
+
writeFileSync,
|
|
56
|
+
} from "node:fs";
|
|
57
|
+
import { homedir } from "node:os";
|
|
58
|
+
import { dirname, join } from "node:path";
|
|
59
|
+
import { fileURLToPath } from "node:url";
|
|
60
|
+
|
|
61
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
62
|
+
const IDLE_MINUTES = 15;
|
|
63
|
+
const MAX_HOURS = 8;
|
|
64
|
+
/** How far a process's start may sit before its token file's startedAt. */
|
|
65
|
+
const START_SKEW_MS = 120 * 1000;
|
|
66
|
+
|
|
67
|
+
const home = (env) => env.HOME || homedir();
|
|
68
|
+
const stateDir = (env) => join(home(env), ".claude", "logs", "multi-agent", "office");
|
|
69
|
+
const stateFile = (env) => join(stateDir(env), "office.json");
|
|
70
|
+
const actionsLog = (env) =>
|
|
71
|
+
join(home(env), ".claude", "logs", "multi-agent", "office-actions.jsonl");
|
|
72
|
+
|
|
73
|
+
const readJson = (p) => {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** The panel page: next to this scripts directory, in a checkout and in the install. */
|
|
82
|
+
export const pagePath = () => join(HERE, "..", "office", "index.html");
|
|
83
|
+
|
|
84
|
+
/** Whether a remote layer is present; none ships with the pipeline. */
|
|
85
|
+
export function remoteLayer(env = process.env) {
|
|
86
|
+
const prefs = readJson(join(home(env), ".claude", "multi-agent-preferences.json"));
|
|
87
|
+
const block = prefs?.global?.office?.remote;
|
|
88
|
+
if (block && typeof block === "object" && !Array.isArray(block)) return true;
|
|
89
|
+
return existsSync(join(home(env), ".claude", "office-remote"));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const remoteText = (env) =>
|
|
93
|
+
remoteLayer(env) ? "provided by a separate remote layer" : "not available in this installation";
|
|
94
|
+
|
|
95
|
+
/** The contract server: beside this script, else the install. */
|
|
96
|
+
export function serverScript(env = process.env) {
|
|
97
|
+
return (
|
|
98
|
+
[
|
|
99
|
+
env.MA_CONTRACT_SERVER,
|
|
100
|
+
join(HERE, "contract-server.mjs"),
|
|
101
|
+
join(home(env), ".claude", "scripts", "contract-server.mjs"),
|
|
102
|
+
]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.find((p) => existsSync(p)) ?? null
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const alive = (pid) => {
|
|
109
|
+
try {
|
|
110
|
+
process.kill(pid, 0);
|
|
111
|
+
return true;
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return e.code === "EPERM";
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
function psField(pid, field) {
|
|
118
|
+
const r = spawnSync("ps", ["-o", `${field}=`, "-p", String(pid)], {
|
|
119
|
+
encoding: "utf8",
|
|
120
|
+
timeout: 5000,
|
|
121
|
+
});
|
|
122
|
+
return r.status === 0 ? r.stdout.trim() : "";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Whether `pid` is still the server a state record names: its token file
|
|
127
|
+
* names the same pid, the process runs contract-server.mjs, and it started
|
|
128
|
+
* no later than the token file and at most START_SKEW_MS before it.
|
|
129
|
+
*/
|
|
130
|
+
export function isOurServer(s, tokenDoc) {
|
|
131
|
+
if (!s || !Number.isInteger(s.pid) || !alive(s.pid)) return false;
|
|
132
|
+
if (!tokenDoc || tokenDoc.pid !== s.pid || typeof tokenDoc.startedAt !== "string") return false;
|
|
133
|
+
if (!psField(s.pid, "command").includes("contract-server.mjs")) return false;
|
|
134
|
+
const began = Date.parse(psField(s.pid, "lstart"));
|
|
135
|
+
const wrote = Date.parse(tokenDoc.startedAt);
|
|
136
|
+
if (!Number.isFinite(began) || !Number.isFinite(wrote)) return false;
|
|
137
|
+
return began <= wrote + 1000 && wrote - began <= START_SKEW_MS;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The server this script started, if it is still running. */
|
|
141
|
+
export function current(env = process.env) {
|
|
142
|
+
const s = readJson(stateFile(env));
|
|
143
|
+
const doc = s ? readJson(s.tokenFile ?? "") : null;
|
|
144
|
+
if (!isOurServer(s, doc) || !doc.token) return null;
|
|
145
|
+
return { ...s, token: doc.token };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** A one-time panel code from the running server (bearer, no Origin). */
|
|
149
|
+
async function panelCode(s) {
|
|
150
|
+
const r = await fetch(`${s.url}/office/code`, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { Authorization: `Bearer ${s.token}` },
|
|
153
|
+
});
|
|
154
|
+
if (!r.ok) throw new Error(`the server refused a panel code (${r.status})`);
|
|
155
|
+
const { code } = await r.json();
|
|
156
|
+
if (typeof code !== "string") throw new Error("the server returned no panel code");
|
|
157
|
+
return code;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeState(env, record) {
|
|
161
|
+
mkdirSync(stateDir(env), { recursive: true, mode: 0o700 });
|
|
162
|
+
chmodSync(stateDir(env), 0o700);
|
|
163
|
+
writeFileSync(stateFile(env), JSON.stringify(record, null, 2) + "\n", { mode: 0o600 });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function waitForReady(file, ms) {
|
|
167
|
+
const until = Date.now() + ms;
|
|
168
|
+
while (Date.now() < until) {
|
|
169
|
+
const text = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
170
|
+
const nl = text.indexOf("\n");
|
|
171
|
+
if (nl !== -1) return JSON.parse(text.slice(0, nl));
|
|
172
|
+
await new Promise((r) => globalThis.setTimeout(r, 100));
|
|
173
|
+
}
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** How long a launcher file lives: the panel code's own lifetime, plus slack. */
|
|
178
|
+
const LAUNCHER_TTL_MS = 15 * 1000;
|
|
179
|
+
const launcherTtl = (env) => {
|
|
180
|
+
const n = Number(env.MA_OFFICE_LAUNCHER_TTL_MS);
|
|
181
|
+
return Number.isInteger(n) && n > 0 && n < LAUNCHER_TTL_MS ? n : LAUNCHER_TTL_MS;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* A 0600 page in the 0700 state directory that forwards to the panel with the
|
|
186
|
+
* code in the fragment. The code lives only in this file and the browser: an
|
|
187
|
+
* opener's argv is readable by every local user (`ps`), a file in a 0700
|
|
188
|
+
* directory is not. Its CSP allows nothing but its own inline script, by hash.
|
|
189
|
+
*/
|
|
190
|
+
export function writeLauncher(env, target) {
|
|
191
|
+
const dir = stateDir(env);
|
|
192
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
193
|
+
chmodSync(dir, 0o700);
|
|
194
|
+
const script = `location.replace(${JSON.stringify(target)})`;
|
|
195
|
+
const hash = createHash("sha256").update(script).digest("base64");
|
|
196
|
+
const html =
|
|
197
|
+
`<!doctype html><meta charset="utf-8">` +
|
|
198
|
+
`<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-${hash}'; base-uri 'none'; form-action 'none'">` +
|
|
199
|
+
`<meta name="referrer" content="no-referrer"><title>Opening the panel</title>` +
|
|
200
|
+
`<script>${script}</script>`;
|
|
201
|
+
const file = join(dir, `launch-${randomBytes(12).toString("hex")}.html`);
|
|
202
|
+
writeFileSync(file, html, { mode: 0o600, flag: "wx" });
|
|
203
|
+
chmodSync(file, 0o600);
|
|
204
|
+
return file;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Launcher files left by an earlier start, past their lifetime, are removed. */
|
|
208
|
+
function sweepLaunchers(env) {
|
|
209
|
+
let names;
|
|
210
|
+
try {
|
|
211
|
+
names = readdirSync(stateDir(env));
|
|
212
|
+
} catch {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
for (const n of names) {
|
|
216
|
+
if (!/^launch-[0-9a-f]{24}\.html$/.test(n)) continue;
|
|
217
|
+
const p = join(stateDir(env), n);
|
|
218
|
+
try {
|
|
219
|
+
if (Date.now() - statSync(p).mtimeMs > launcherTtl(env)) rmSync(p, { force: true });
|
|
220
|
+
} catch {
|
|
221
|
+
// Already gone.
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Removes the launcher after its lifetime, from a detached process whose argv names only the path. */
|
|
227
|
+
function scheduleRemoval(file, env) {
|
|
228
|
+
const child = spawn(
|
|
229
|
+
process.execPath,
|
|
230
|
+
[
|
|
231
|
+
"-e",
|
|
232
|
+
"setTimeout(() => { try { require('fs').rmSync(process.argv[1], { force: true }); } catch {} }, Number(process.argv[2]))",
|
|
233
|
+
file,
|
|
234
|
+
String(launcherTtl(env)),
|
|
235
|
+
],
|
|
236
|
+
{ detached: true, stdio: "ignore" },
|
|
237
|
+
);
|
|
238
|
+
child.unref();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function openBrowser(target, env) {
|
|
242
|
+
if (env.MA_OFFICE_NO_BROWSER === "1") return false;
|
|
243
|
+
sweepLaunchers(env);
|
|
244
|
+
const file = writeLauncher(env, target);
|
|
245
|
+
scheduleRemoval(file, env);
|
|
246
|
+
const bin = env.MA_OFFICE_OPENER || (process.platform === "darwin" ? "open" : "xdg-open");
|
|
247
|
+
const r = spawnSync(bin, [`file://${file}`], { stdio: "ignore", timeout: 15000 });
|
|
248
|
+
return r.status === 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Start (or reuse) the panel server and open the browser. */
|
|
252
|
+
export async function officeOn({ env = process.env, open = true } = {}) {
|
|
253
|
+
let s = current(env);
|
|
254
|
+
let reused = true;
|
|
255
|
+
if (!s) {
|
|
256
|
+
reused = false;
|
|
257
|
+
const server = serverScript(env);
|
|
258
|
+
if (!server) throw new Error("contract-server.mjs was not found; install the pipeline first");
|
|
259
|
+
mkdirSync(stateDir(env), { recursive: true, mode: 0o700 });
|
|
260
|
+
const ready = join(stateDir(env), "ready.json");
|
|
261
|
+
rmSync(ready, { force: true });
|
|
262
|
+
const out = openSync(ready, "w", 0o600);
|
|
263
|
+
const child = spawn(
|
|
264
|
+
process.execPath,
|
|
265
|
+
[
|
|
266
|
+
server,
|
|
267
|
+
"--port",
|
|
268
|
+
"0",
|
|
269
|
+
"--idle-minutes",
|
|
270
|
+
String(IDLE_MINUTES),
|
|
271
|
+
"--max-hours",
|
|
272
|
+
String(MAX_HOURS),
|
|
273
|
+
],
|
|
274
|
+
{
|
|
275
|
+
env: { ...env, MA_OFFICE_PAGE: env.MA_OFFICE_PAGE || pagePath() },
|
|
276
|
+
detached: true,
|
|
277
|
+
stdio: ["ignore", out, "ignore"],
|
|
278
|
+
},
|
|
279
|
+
);
|
|
280
|
+
child.unref();
|
|
281
|
+
const info = await waitForReady(ready, 15000);
|
|
282
|
+
rmSync(ready, { force: true });
|
|
283
|
+
if (!info) throw new Error("the server did not report ready");
|
|
284
|
+
writeState(env, {
|
|
285
|
+
pid: info.pid,
|
|
286
|
+
port: info.port,
|
|
287
|
+
url: info.url,
|
|
288
|
+
tokenFile: info.tokenFile,
|
|
289
|
+
idleMinutes: info.idleMinutes,
|
|
290
|
+
maxHours: info.maxHours ?? null,
|
|
291
|
+
startedAt: new Date().toISOString(),
|
|
292
|
+
});
|
|
293
|
+
s = current(env);
|
|
294
|
+
if (!s) throw new Error("the server stopped right after it started");
|
|
295
|
+
}
|
|
296
|
+
const page = `${s.url}/office`;
|
|
297
|
+
const opened = open ? openBrowser(`${page}#code=${await panelCode(s)}`, env) : false;
|
|
298
|
+
return { running: true, reused, url: page, pid: s.pid, idleMinutes: s.idleMinutes, opened };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Stop the server this script started. */
|
|
302
|
+
export function officeOff({ env = process.env } = {}) {
|
|
303
|
+
const s = readJson(stateFile(env));
|
|
304
|
+
let stopped = false;
|
|
305
|
+
if (isOurServer(s, s ? readJson(s.tokenFile ?? "") : null)) {
|
|
306
|
+
try {
|
|
307
|
+
process.kill(s.pid, "SIGTERM");
|
|
308
|
+
stopped = true;
|
|
309
|
+
} catch {
|
|
310
|
+
stopped = false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
rmSync(stateFile(env), { force: true });
|
|
314
|
+
return { running: false, stopped, remote: remoteText(env) };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function devices(env) {
|
|
318
|
+
const reg = readJson(join(home(env), ".claude", "logs", "multi-agent", "phone", "devices.json"));
|
|
319
|
+
return Array.isArray(reg?.devices)
|
|
320
|
+
? reg.devices.filter((d) => d && !d.revokedAt).map((d) => ({ name: String(d.name ?? d.id) }))
|
|
321
|
+
: [];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** What is running, and what the panel did recently. */
|
|
325
|
+
export function officeStatus({ env = process.env, last = 10 } = {}) {
|
|
326
|
+
const s = current(env);
|
|
327
|
+
let actions;
|
|
328
|
+
try {
|
|
329
|
+
actions = readFileSync(actionsLog(env), "utf8")
|
|
330
|
+
.trim()
|
|
331
|
+
.split("\n")
|
|
332
|
+
.filter(Boolean)
|
|
333
|
+
.slice(-last)
|
|
334
|
+
.map((l) => {
|
|
335
|
+
const a = JSON.parse(l);
|
|
336
|
+
return { at: a.at, via: a.via, route: a.route, step: a.step, status: a.status };
|
|
337
|
+
});
|
|
338
|
+
} catch {
|
|
339
|
+
actions = [];
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
running: Boolean(s),
|
|
343
|
+
url: s ? `${s.url}/office` : null,
|
|
344
|
+
pid: s?.pid ?? null,
|
|
345
|
+
idleMinutes: s?.idleMinutes ?? null,
|
|
346
|
+
remote: remoteText(env),
|
|
347
|
+
pairedDevices: devices(env),
|
|
348
|
+
recentActions: actions,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async function main(argv) {
|
|
353
|
+
const [cmd, ...rest] = argv;
|
|
354
|
+
const json = rest.includes("--json");
|
|
355
|
+
const flags = rest.filter((a) => a !== "--json");
|
|
356
|
+
let out;
|
|
357
|
+
try {
|
|
358
|
+
if (cmd === "on" && flags.every((f) => f === "--no-open")) {
|
|
359
|
+
out = await officeOn({ open: !flags.includes("--no-open") });
|
|
360
|
+
} else if (cmd === "off" && !flags.length) out = officeOff();
|
|
361
|
+
else if (cmd === "status" && !flags.length) out = officeStatus();
|
|
362
|
+
else {
|
|
363
|
+
process.stderr.write("usage: office-control.mjs on [--no-open] | off | status [--json]\n");
|
|
364
|
+
return 2;
|
|
365
|
+
}
|
|
366
|
+
} catch (err) {
|
|
367
|
+
process.stderr.write(`office-control: ${err.message}\n`);
|
|
368
|
+
return 1;
|
|
369
|
+
}
|
|
370
|
+
if (json) process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
371
|
+
else {
|
|
372
|
+
for (const [k, v] of Object.entries(out)) {
|
|
373
|
+
process.stdout.write(`${k}: ${typeof v === "object" ? JSON.stringify(v) : v}\n`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return 0;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
380
|
+
main(process.argv.slice(2)).then((code) => {
|
|
381
|
+
process.exitCode = code;
|
|
382
|
+
});
|
|
383
|
+
}
|