@mmerterden/multi-agent-pipeline 20.29.0 → 20.33.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 +86 -0
- package/README.md +50 -5
- package/README.tr.md +64 -5
- package/docs/architecture.md +3 -3
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +9 -9
- package/install/_unattended-profile.mjs +61 -0
- package/install/catalog-history.json +1 -1
- package/install/claude.mjs +19 -3
- package/install/codex.mjs +2 -0
- package/install/copilot.mjs +13 -0
- package/install/index.mjs +10 -1
- package/install/templates/claude-hooks.json +12 -1
- package/manifest.json +230 -88
- package/package.json +3 -1
- package/pipeline/commands/multi-agent/analysis/SKILL.md +4 -4
- 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 +51 -0
- package/pipeline/commands/multi-agent/office-status/SKILL.md +29 -0
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +4 -1
- package/pipeline/commands/multi-agent/review/SKILL.md +4 -3
- package/pipeline/commands/multi-agent/scenario-audit/SKILL.md +4 -2
- 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 +15 -3
- package/pipeline/commands/multi-agent/test-gen/SKILL.md +29 -3
- package/pipeline/contract/CHANGELOG.md +108 -0
- package/pipeline/contract/README.md +72 -12
- 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 +506 -27
- package/pipeline/contract/types/index.d.ts +829 -4
- 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/cross-cli-contract.md +5 -4
- package/pipeline/multi-agent-refs/features/office-threat-model.md +109 -0
- package/pipeline/multi-agent-refs/features/unattended-security.md +1 -1
- 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/modes.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +16 -10
- package/pipeline/multi-agent-refs/review-standalone-tracker.md +37 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +8 -3
- package/pipeline/office/NOTICE.md +45 -0
- package/pipeline/office/assets/ARCHIVE_BOXES.png +0 -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_MACHINE.png +0 -0
- package/pipeline/office/assets/COFFEE_TABLE.png +0 -0
- package/pipeline/office/assets/COUNTER_SINK.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/DEVICE_STAND.png +0 -0
- package/pipeline/office/assets/DOUBLE_BOOKSHELF.png +0 -0
- package/pipeline/office/assets/FILING_CABINET.png +0 -0
- package/pipeline/office/assets/FRIDGE.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/MAILBOX.png +0 -0
- package/pipeline/office/assets/MOP_BUCKET.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/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/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/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/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 +8431 -0
- package/pipeline/office/toolkit.json +3 -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 +613 -0
- package/pipeline/schemas/agent-state.schema.json +493 -115
- 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 +45 -4
- package/pipeline/schemas/issue-maturity.schema.json +68 -0
- 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/ledger.schema.json +202 -0
- 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/permissions.schema.json +47 -0
- package/pipeline/schemas/phases.json +10 -2
- package/pipeline/schemas/phases.schema.json +86 -19
- package/pipeline/schemas/prefs.schema.json +19 -0
- package/pipeline/schemas/prs.schema.json +78 -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/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 +257 -46
- 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/system-status.schema.json +111 -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 +65 -0
- package/pipeline/schemas/unattended-profile.json +7 -2
- package/pipeline/scripts/_active-run.mjs +2 -1
- package/pipeline/scripts/_cost.mjs +66 -1
- package/pipeline/scripts/_office-smoke-lib.sh +115 -0
- package/pipeline/scripts/_spawn-async.mjs +77 -0
- package/pipeline/scripts/_transcript-tail.mjs +274 -0
- package/pipeline/scripts/activity-snapshot.mjs +1223 -0
- package/pipeline/scripts/autopilot-control.mjs +214 -9
- package/pipeline/scripts/autopilot-runner.mjs +7 -5
- package/pipeline/scripts/contract-server.mjs +1853 -197
- package/pipeline/scripts/cost-analyze.mjs +2 -53
- package/pipeline/scripts/cost-table.json +1 -0
- package/pipeline/scripts/gen-mode-dispatch.mjs +3 -1
- package/pipeline/scripts/job-launch.mjs +477 -0
- package/pipeline/scripts/launch-request.mjs +94 -9
- package/pipeline/scripts/office-build.mjs +226 -0
- package/pipeline/scripts/office-control.mjs +410 -0
- package/pipeline/scripts/office-credentials.mjs +106 -0
- package/pipeline/scripts/office-ledger.mjs +409 -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/prune-logs.sh +53 -2
- package/pipeline/scripts/routine-registry.mjs +42 -6
- package/pipeline/scripts/run-results.mjs +373 -0
- package/pipeline/scripts/runs-index.mjs +62 -2
- 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/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 +625 -0
- package/pipeline/scripts/transcript-index.mjs +4 -1
- package/pipeline/scripts/uninstall.mjs +3 -0
- package/pipeline/scripts/work-items.mjs +473 -0
- package/pipeline/skills/.skill-manifest.json +23 -11
- 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 +21 -0
- package/pipeline/skills/shared/core/multi-agent-office-status/SKILL.md +17 -0
- package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +4 -1
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +19 -0
- package/pipeline/skills/shared/core/multi-agent-scenario-audit/SKILL.md +4 -2
- 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/shared/core/multi-agent-test-gen/SKILL.md +26 -3
- package/pipeline/skills/skills-index.md +4 -1
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file toolkit-tools.mjs - the multi-agent-toolkit MCP server's tools, for
|
|
4
|
+
* a client that is not an MCP host: list them, and call one with arguments its
|
|
5
|
+
* own inputSchema accepts.
|
|
6
|
+
*
|
|
7
|
+
* The server is the one the user registered with Claude Code: the
|
|
8
|
+
* `mcpServers` entry whose name contains "toolkit" in ~/.claude.json, else in
|
|
9
|
+
* ~/.claude/settings.json (the lookup doctor.mjs makes). It is started as a
|
|
10
|
+
* stdio child with that entry's command, arguments and environment on top of
|
|
11
|
+
* this process's, so every guard the toolkit honours from the environment
|
|
12
|
+
* (MULTI_AGENT_UNATTENDED, its URL policy) applies unchanged, and a tool's own
|
|
13
|
+
* confirm or preview behaviour stays the tool's. One child per client, started
|
|
14
|
+
* on first use and closed with it.
|
|
15
|
+
*
|
|
16
|
+
* Each tool carries `risk` from its own annotations: readOnlyHint is read,
|
|
17
|
+
* destructiveHint is high, anything else is low.
|
|
18
|
+
*
|
|
19
|
+
* Calls are denied by default. A tool may be called only when it declares
|
|
20
|
+
* readOnlyHint and its inputSchema takes nothing that names a place: no
|
|
21
|
+
* property whose name speaks of a path, URL, file, directory, output, project
|
|
22
|
+
* or root, and no uri format, at any depth. Everything else is listed with
|
|
23
|
+
* `allowed: false` and refused with `tool-not-allowed`, because a read-only
|
|
24
|
+
* tool that is handed a location reads whatever the location points at (a
|
|
25
|
+
* local file, a loopback service). Arguments may not carry `confirm`, which
|
|
26
|
+
* would skip a tool's own preview.
|
|
27
|
+
*
|
|
28
|
+
* The child always runs with MULTI_AGENT_UNATTENDED=1 and
|
|
29
|
+
* MCP_TOOLKIT_URL_POLICY=strict, set after the registration's own environment
|
|
30
|
+
* so neither can be overridden. A registration whose package spec has no exact
|
|
31
|
+
* version (bare, a dist-tag such as `@latest`, or a range) is pinned to
|
|
32
|
+
* MA_TOOLKIT_VERSION, else prefs.global.office.toolkitVersion, else the
|
|
33
|
+
* version the release ships beside the office page (office/toolkit.json, the
|
|
34
|
+
* one it was checked against), and refused when none is set.
|
|
35
|
+
*
|
|
36
|
+
* No tool that is not read-only is allowed today. Opening one up means routing
|
|
37
|
+
* it through the server's two-step confirm, never a direct call.
|
|
38
|
+
*
|
|
39
|
+
* Arguments are checked against the tool's inputSchema before the call (the
|
|
40
|
+
* subset the toolkit's schemas use: type, enum, const, required, properties,
|
|
41
|
+
* additionalProperties, items, min/max, minLength/maxLength, pattern). A key
|
|
42
|
+
* the schema does not declare is refused at any depth, whatever
|
|
43
|
+
* additionalProperties says, and so is a `confirm` key. An unknown tool or
|
|
44
|
+
* invalid arguments never reach the server. Results come back as text
|
|
45
|
+
* (redacted), JSON (redacted, as text) and images (base64, image types only,
|
|
46
|
+
* size-capped); a tool's error message is redacted too.
|
|
47
|
+
*
|
|
48
|
+
* Usage:
|
|
49
|
+
* toolkit-tools.mjs list
|
|
50
|
+
* toolkit-tools.mjs call <name> '<json arguments>'
|
|
51
|
+
*
|
|
52
|
+
* MA_TOOLKIT_MCP sets the server as JSON {command, args, env} (tests).
|
|
53
|
+
*
|
|
54
|
+
* Exit: 0 printed, 1 refused or failed, 2 usage.
|
|
55
|
+
*
|
|
56
|
+
* @module pipeline/scripts/toolkit-tools
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
import { spawn } from "node:child_process";
|
|
60
|
+
import { readFileSync } from "node:fs";
|
|
61
|
+
import { homedir } from "node:os";
|
|
62
|
+
import { basename, dirname, join } from "node:path";
|
|
63
|
+
import { clearTimeout, setTimeout } from "node:timers";
|
|
64
|
+
import { createRedactor } from "./transcript-index.mjs";
|
|
65
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
66
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
67
|
+
|
|
68
|
+
export const CONTRACT_VERSION = "1.0.0";
|
|
69
|
+
export const TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
70
|
+
const START_TIMEOUT_MS = 60_000;
|
|
71
|
+
const CALL_TIMEOUT_MS = 180_000;
|
|
72
|
+
const MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
73
|
+
const MAX_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
74
|
+
const IMAGE_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
75
|
+
|
|
76
|
+
export class ToolError extends Error {
|
|
77
|
+
constructor(code, message, extra = {}) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.code = code;
|
|
80
|
+
this.extra = extra;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const readJson = (p) => {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** The registered toolkit server, or null. */
|
|
93
|
+
export function toolkitEntry(env = process.env) {
|
|
94
|
+
if (env.MA_TOOLKIT_MCP) {
|
|
95
|
+
try {
|
|
96
|
+
const e = JSON.parse(env.MA_TOOLKIT_MCP);
|
|
97
|
+
if (e && typeof e.command === "string") return e;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const home = env.HOME || homedir();
|
|
103
|
+
for (const p of [join(home, ".claude.json"), join(home, ".claude", "settings.json")]) {
|
|
104
|
+
const j = readJson(p);
|
|
105
|
+
const key = j?.mcpServers && Object.keys(j.mcpServers).find((k) => /toolkit/i.test(k));
|
|
106
|
+
if (key && typeof j.mcpServers[key]?.command === "string") return j.mcpServers[key];
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Families refused whatever their schema says: `web` tools share a browser
|
|
113
|
+
* whose page may already point at a local file or service, and `context`
|
|
114
|
+
* tools search an index built from whatever files were indexed.
|
|
115
|
+
*/
|
|
116
|
+
const DENIED_CATEGORIES = new Set(["web", "context"]);
|
|
117
|
+
|
|
118
|
+
/** Property names that hand a tool a location to read or write. */
|
|
119
|
+
const LOCATION_NAME =
|
|
120
|
+
/(path|url|uri|href|link|file|dir|folder|output|out_?dir|project|root|cwd|workspace|location|endpoint|host|address|socket|device|bundle|archive|repo)/i;
|
|
121
|
+
const LOCATION_FORMAT = /^(uri|uri-reference|iri|iri-reference|url|hostname|idn-hostname)$/i;
|
|
122
|
+
|
|
123
|
+
/** The location-shaped properties anywhere in a schema, by name or format. */
|
|
124
|
+
export function locationProperties(schema, prefix = "") {
|
|
125
|
+
const out = [];
|
|
126
|
+
if (!schema || typeof schema !== "object") return out;
|
|
127
|
+
for (const [k, v] of Object.entries(schema.properties ?? {})) {
|
|
128
|
+
const at = prefix ? `${prefix}.${k}` : k;
|
|
129
|
+
if (LOCATION_NAME.test(k) || LOCATION_FORMAT.test(String(v?.format ?? ""))) out.push(at);
|
|
130
|
+
out.push(...locationProperties(v, at));
|
|
131
|
+
if (v?.items) out.push(...locationProperties(v.items, `${at}[]`));
|
|
132
|
+
}
|
|
133
|
+
for (const key of ["anyOf", "oneOf", "allOf"]) {
|
|
134
|
+
for (const alt of Array.isArray(schema[key]) ? schema[key] : []) {
|
|
135
|
+
out.push(...locationProperties(alt, prefix));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
139
|
+
out.push(...locationProperties(schema.additionalProperties, `${prefix}*`));
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Whether a tool may be called from this client, and why not. Only a
|
|
146
|
+
* read-only tool that takes no location is allowed.
|
|
147
|
+
*/
|
|
148
|
+
export function callPolicy(tool) {
|
|
149
|
+
if (DENIED_CATEGORIES.has(categoryOf(String(tool?.name ?? "")))) {
|
|
150
|
+
return {
|
|
151
|
+
allowed: false,
|
|
152
|
+
reason:
|
|
153
|
+
"this family reads state shared with other sessions (a browser, an index of files); call it from a terminal session",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
if (tool?.annotations?.readOnlyHint !== true || riskOf(tool) !== "read") {
|
|
157
|
+
return {
|
|
158
|
+
allowed: false,
|
|
159
|
+
reason: "only tools that declare readOnlyHint, and nothing destructive, can be called here",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const where = locationProperties(tool.inputSchema);
|
|
163
|
+
if (where.length) {
|
|
164
|
+
return {
|
|
165
|
+
allowed: false,
|
|
166
|
+
reason: `the tool takes a location (${where.slice(0, 3).join(", ")}); call it from a terminal session`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return { allowed: true, reason: null };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Environment the toolkit child cannot override: unattended guards, strict URL policy. */
|
|
173
|
+
export const FORCED_ENV = Object.freeze({
|
|
174
|
+
MULTI_AGENT_UNATTENDED: "1",
|
|
175
|
+
MCP_TOOLKIT_URL_POLICY: "strict",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const SEMVER = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
179
|
+
const RUNNERS = new Set(["npx", "bunx", "pnpx"]);
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The package specs a registration hands to a package runner (npx, bunx,
|
|
183
|
+
* pnpx, `npm exec`, `pnpm dlx`): the first positional argument, or each
|
|
184
|
+
* `-p` / `--package` value. A registration that runs a local file has none.
|
|
185
|
+
*/
|
|
186
|
+
export function packageSpecs(entry) {
|
|
187
|
+
const cmd = basename(String(entry?.command ?? ""));
|
|
188
|
+
const args = Array.isArray(entry?.args) ? entry.args.map(String) : [];
|
|
189
|
+
let rest;
|
|
190
|
+
if (RUNNERS.has(cmd)) rest = args;
|
|
191
|
+
else if (cmd === "npm" && (args[0] === "exec" || args[0] === "x")) rest = args.slice(1);
|
|
192
|
+
else if (cmd === "pnpm" && args[0] === "dlx") rest = args.slice(1);
|
|
193
|
+
else return [];
|
|
194
|
+
const specs = [];
|
|
195
|
+
let named = false;
|
|
196
|
+
for (let i = 0; i < rest.length; i++) {
|
|
197
|
+
const a = rest[i];
|
|
198
|
+
if (a === "-p" || a === "--package") {
|
|
199
|
+
if (rest[i + 1] !== undefined) specs.push(rest[++i]);
|
|
200
|
+
named = true;
|
|
201
|
+
} else if (a.startsWith("--package=")) {
|
|
202
|
+
specs.push(a.slice("--package=".length));
|
|
203
|
+
named = true;
|
|
204
|
+
} else if (a === "--") {
|
|
205
|
+
if (!named && rest[i + 1] !== undefined) specs.push(rest[i + 1]);
|
|
206
|
+
break;
|
|
207
|
+
} else if (!a.startsWith("-")) {
|
|
208
|
+
if (!named) specs.push(a);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return specs;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The name and version of a spec: `@scope/name@1.2.3`, `name@^1`, `name`. */
|
|
216
|
+
function splitSpec(spec) {
|
|
217
|
+
const at = spec.lastIndexOf("@");
|
|
218
|
+
return at > 0 ? [spec.slice(0, at), spec.slice(at + 1)] : [spec, null];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Whether a registration would resolve its package anew on every start: a
|
|
223
|
+
* spec without an exact version (bare, a dist-tag such as latest or next, or
|
|
224
|
+
* a range) is unpinned.
|
|
225
|
+
*/
|
|
226
|
+
export function unpinned(entry) {
|
|
227
|
+
return packageSpecs(entry).some((s) => !SEMVER.test(splitSpec(s)[1] ?? ""));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The registration with every unpinned spec set to `<name>@<version>`, or
|
|
232
|
+
* null when it is unpinned and no exact version is known. The version comes
|
|
233
|
+
* from MA_TOOLKIT_VERSION, else prefs.global.office.toolkitVersion, else
|
|
234
|
+
* office/toolkit.json beside the page (MA_OFFICE_PAGE's directory, or the
|
|
235
|
+
* office directory next to this script's).
|
|
236
|
+
*/
|
|
237
|
+
export function pinnedEntry(entry, env = process.env) {
|
|
238
|
+
if (!unpinned(entry)) return entry;
|
|
239
|
+
const prefs = readJson(join(env.HOME || homedir(), ".claude", "multi-agent-preferences.json"));
|
|
240
|
+
const version =
|
|
241
|
+
env.MA_TOOLKIT_VERSION || prefs?.global?.office?.toolkitVersion || shippedToolkitVersion(env);
|
|
242
|
+
if (typeof version !== "string" || !SEMVER.test(version)) return null;
|
|
243
|
+
const loose = new Set(packageSpecs(entry).filter((s) => !SEMVER.test(splitSpec(s)[1] ?? "")));
|
|
244
|
+
const pin = (a) => {
|
|
245
|
+
if (typeof a !== "string") return a;
|
|
246
|
+
if (loose.has(a)) return `${splitSpec(a)[0]}@${version}`;
|
|
247
|
+
if (a.startsWith("--package=") && loose.has(a.slice(10))) {
|
|
248
|
+
return `--package=${splitSpec(a.slice(10))[0]}@${version}`;
|
|
249
|
+
}
|
|
250
|
+
return a;
|
|
251
|
+
};
|
|
252
|
+
return { ...entry, args: Array.isArray(entry.args) ? entry.args.map(pin) : entry.args };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The toolkit version the release ships beside the office page, or undefined. */
|
|
256
|
+
export function shippedToolkitVersion(env = process.env) {
|
|
257
|
+
const dir = env.MA_OFFICE_PAGE
|
|
258
|
+
? dirname(env.MA_OFFICE_PAGE)
|
|
259
|
+
: join(import.meta.dirname, "..", "office");
|
|
260
|
+
const v = readJson(join(dir, "toolkit.json"))?.toolkitVersion;
|
|
261
|
+
return typeof v === "string" && SEMVER.test(v) ? v : undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** The exact toolkit version the Tools window starts, or null when none is known. */
|
|
265
|
+
export function activeToolkitVersion(env = process.env) {
|
|
266
|
+
const registered = toolkitEntry(env);
|
|
267
|
+
const entry = registered ? pinnedEntry(registered, env) : null;
|
|
268
|
+
const spec = entry ? packageSpecs(entry)[0] : null;
|
|
269
|
+
const v = spec ? splitSpec(spec)[1] : null;
|
|
270
|
+
return typeof v === "string" && SEMVER.test(v) ? v : null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** read, low or high, from the tool's own annotations. */
|
|
274
|
+
export function riskOf(tool) {
|
|
275
|
+
const a = tool?.annotations ?? {};
|
|
276
|
+
if (a.destructiveHint === true) return "high";
|
|
277
|
+
if (a.readOnlyHint === true) return "read";
|
|
278
|
+
return "low";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The tool's category: the prefix before its first underscore. */
|
|
282
|
+
export const categoryOf = (name) => (name.includes("_") ? name.split("_")[0] : "other");
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Every reason `value` does not satisfy `schema`, for the subset of JSON
|
|
286
|
+
* Schema the toolkit's input schemas use. Unknown keywords are ignored; the
|
|
287
|
+
* server validates again.
|
|
288
|
+
*/
|
|
289
|
+
export function schemaErrors(schema, value, path = "", opts = {}) {
|
|
290
|
+
if (!schema || typeof schema !== "object") return [];
|
|
291
|
+
const at = path || "/";
|
|
292
|
+
const errors = [];
|
|
293
|
+
const types = schema.type === undefined ? null : [].concat(schema.type);
|
|
294
|
+
const typeOf = (v) =>
|
|
295
|
+
v === null ? "null" : Array.isArray(v) ? "array" : Number.isInteger(v) ? "integer" : typeof v;
|
|
296
|
+
if (types) {
|
|
297
|
+
const t = typeOf(value);
|
|
298
|
+
const ok = types.some((x) => x === t || (x === "number" && t === "integer"));
|
|
299
|
+
if (!ok) return [`${at} must be ${types.join(" or ")}`];
|
|
300
|
+
}
|
|
301
|
+
if (
|
|
302
|
+
Array.isArray(schema.enum) &&
|
|
303
|
+
!schema.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))
|
|
304
|
+
) {
|
|
305
|
+
errors.push(`${at} must be one of ${schema.enum.map((e) => JSON.stringify(e)).join(", ")}`);
|
|
306
|
+
}
|
|
307
|
+
if ("const" in schema && JSON.stringify(schema.const) !== JSON.stringify(value)) {
|
|
308
|
+
errors.push(`${at} must be ${JSON.stringify(schema.const)}`);
|
|
309
|
+
}
|
|
310
|
+
if (typeof value === "string") {
|
|
311
|
+
if (Number.isInteger(schema.minLength) && value.length < schema.minLength)
|
|
312
|
+
errors.push(`${at} is too short`);
|
|
313
|
+
if (Number.isInteger(schema.maxLength) && value.length > schema.maxLength)
|
|
314
|
+
errors.push(`${at} is too long`);
|
|
315
|
+
if (typeof schema.pattern === "string") {
|
|
316
|
+
try {
|
|
317
|
+
if (!new RegExp(schema.pattern, "u").test(value))
|
|
318
|
+
errors.push(`${at} does not match its pattern`);
|
|
319
|
+
} catch {
|
|
320
|
+
// A pattern this engine cannot compile is left to the server.
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (typeof value === "number") {
|
|
325
|
+
if (typeof schema.minimum === "number" && value < schema.minimum)
|
|
326
|
+
errors.push(`${at} is below ${schema.minimum}`);
|
|
327
|
+
if (typeof schema.maximum === "number" && value > schema.maximum)
|
|
328
|
+
errors.push(`${at} is above ${schema.maximum}`);
|
|
329
|
+
}
|
|
330
|
+
if (Array.isArray(value)) {
|
|
331
|
+
if (Number.isInteger(schema.maxItems) && value.length > schema.maxItems)
|
|
332
|
+
errors.push(`${at} has too many items`);
|
|
333
|
+
if (Number.isInteger(schema.minItems) && value.length < schema.minItems)
|
|
334
|
+
errors.push(`${at} has too few items`);
|
|
335
|
+
if (schema.items && typeof schema.items === "object") {
|
|
336
|
+
value.forEach((v, i) => errors.push(...schemaErrors(schema.items, v, `${path}/${i}`, opts)));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
340
|
+
const props = schema.properties ?? {};
|
|
341
|
+
for (const k of Array.isArray(schema.required) ? schema.required : []) {
|
|
342
|
+
if (!(k in value)) errors.push(`${path}/${k} is required`);
|
|
343
|
+
}
|
|
344
|
+
for (const [k, v] of Object.entries(value)) {
|
|
345
|
+
if (Object.hasOwn(props, k)) errors.push(...schemaErrors(props[k], v, `${path}/${k}`, opts));
|
|
346
|
+
else if (opts.strict) errors.push(`${path}/${k} is not declared by the tool's inputSchema`);
|
|
347
|
+
else if (schema.additionalProperties === false) errors.push(`${path}/${k} is not allowed`);
|
|
348
|
+
else if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
349
|
+
errors.push(...schemaErrors(schema.additionalProperties, v, `${path}/${k}`, opts));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return errors;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Every `confirm` key in an argument value, at any depth, as JSON pointers. */
|
|
357
|
+
export function confirmPaths(value, path = "") {
|
|
358
|
+
if (Array.isArray(value)) return value.flatMap((v, i) => confirmPaths(v, `${path}/${i}`));
|
|
359
|
+
if (!value || typeof value !== "object") return [];
|
|
360
|
+
return Object.entries(value).flatMap(([k, v]) => [
|
|
361
|
+
...(k === "confirm" ? [`${path}/confirm`] : []),
|
|
362
|
+
...confirmPaths(v, `${path}/${k}`),
|
|
363
|
+
]);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* A stdio MCP client for the toolkit, started lazily, one per caller.
|
|
368
|
+
*
|
|
369
|
+
* @param {{env?: object}} [opts]
|
|
370
|
+
*/
|
|
371
|
+
export function createToolkitClient({ env = process.env } = {}) {
|
|
372
|
+
let child = null;
|
|
373
|
+
let ready = null;
|
|
374
|
+
let nextId = 1;
|
|
375
|
+
let buf = "";
|
|
376
|
+
let tools = null;
|
|
377
|
+
const pending = new Map();
|
|
378
|
+
|
|
379
|
+
const failAll = (err) => {
|
|
380
|
+
for (const p of pending.values()) {
|
|
381
|
+
clearTimeout(p.timer);
|
|
382
|
+
p.reject(err);
|
|
383
|
+
}
|
|
384
|
+
pending.clear();
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const rpc = (method, params, timeoutMs) =>
|
|
388
|
+
new Promise((resolve, reject) => {
|
|
389
|
+
const id = nextId++;
|
|
390
|
+
const timer = setTimeout(() => {
|
|
391
|
+
pending.delete(id);
|
|
392
|
+
reject(new ToolError("timeout", `the toolkit did not answer ${method} in time`));
|
|
393
|
+
}, timeoutMs);
|
|
394
|
+
pending.set(id, { resolve, reject, timer });
|
|
395
|
+
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
const start = () => {
|
|
399
|
+
if (ready) return ready;
|
|
400
|
+
const registered = toolkitEntry(env);
|
|
401
|
+
if (!registered) {
|
|
402
|
+
return Promise.reject(
|
|
403
|
+
new ToolError("not-registered", "multi-agent-toolkit is not registered as an MCP server"),
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
const entry = pinnedEntry(registered, env);
|
|
407
|
+
if (!entry) {
|
|
408
|
+
return Promise.reject(
|
|
409
|
+
new ToolError(
|
|
410
|
+
"unpinned",
|
|
411
|
+
"no exact toolkit version is known: the release ships it in office/toolkit.json beside the page, which reinstalling (node install.js) restores; MA_TOOLKIT_VERSION or prefs.global.office.toolkitVersion override it",
|
|
412
|
+
),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
ready = new Promise((resolve, reject) => {
|
|
416
|
+
child = spawn(entry.command, Array.isArray(entry.args) ? entry.args : [], {
|
|
417
|
+
env: { ...env, ...(entry.env ?? {}), ...FORCED_ENV },
|
|
418
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
419
|
+
});
|
|
420
|
+
child.on("error", (e) => {
|
|
421
|
+
ready = null;
|
|
422
|
+
reject(
|
|
423
|
+
new ToolError("not-started", `the toolkit could not start (${e.code ?? e.message})`),
|
|
424
|
+
);
|
|
425
|
+
});
|
|
426
|
+
child.on("exit", () => {
|
|
427
|
+
ready = null;
|
|
428
|
+
tools = null;
|
|
429
|
+
failAll(new ToolError("exited", "the toolkit exited"));
|
|
430
|
+
});
|
|
431
|
+
child.stdout.setEncoding("utf8");
|
|
432
|
+
child.stdout.on("data", (d) => {
|
|
433
|
+
buf += d;
|
|
434
|
+
if (buf.length > MAX_LINE_BYTES) {
|
|
435
|
+
buf = "";
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
let nl;
|
|
439
|
+
while ((nl = buf.indexOf("\n")) !== -1) {
|
|
440
|
+
const line = buf.slice(0, nl);
|
|
441
|
+
buf = buf.slice(nl + 1);
|
|
442
|
+
let msg;
|
|
443
|
+
try {
|
|
444
|
+
msg = JSON.parse(line);
|
|
445
|
+
} catch {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const p = msg && pending.get(msg.id);
|
|
449
|
+
if (!p) continue;
|
|
450
|
+
pending.delete(msg.id);
|
|
451
|
+
clearTimeout(p.timer);
|
|
452
|
+
if (msg.error) {
|
|
453
|
+
const why = createRedactor().clean(String(msg.error.message ?? "the tool failed"));
|
|
454
|
+
p.reject(new ToolError("tool-error", why.slice(0, 2000)));
|
|
455
|
+
} else p.resolve(msg.result);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
rpc(
|
|
459
|
+
"initialize",
|
|
460
|
+
{
|
|
461
|
+
protocolVersion: "2025-06-18",
|
|
462
|
+
capabilities: {},
|
|
463
|
+
clientInfo: { name: "multi-agent-contract-server", version: CONTRACT_VERSION },
|
|
464
|
+
},
|
|
465
|
+
START_TIMEOUT_MS,
|
|
466
|
+
)
|
|
467
|
+
.then(() => {
|
|
468
|
+
child.stdin.write(
|
|
469
|
+
JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n",
|
|
470
|
+
);
|
|
471
|
+
resolve();
|
|
472
|
+
})
|
|
473
|
+
.catch((e) => {
|
|
474
|
+
ready = null;
|
|
475
|
+
reject(e);
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
return ready;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
/** Every tool with its category, risk, description and inputSchema. */
|
|
482
|
+
async function listTools() {
|
|
483
|
+
if (tools) return tools;
|
|
484
|
+
await start();
|
|
485
|
+
const all = [];
|
|
486
|
+
let cursor;
|
|
487
|
+
for (let page = 0; page < 20; page++) {
|
|
488
|
+
const r = await rpc("tools/list", cursor ? { cursor } : {}, START_TIMEOUT_MS);
|
|
489
|
+
for (const t of Array.isArray(r?.tools) ? r.tools : []) {
|
|
490
|
+
if (typeof t?.name !== "string" || !TOOL_NAME.test(t.name)) continue;
|
|
491
|
+
all.push({
|
|
492
|
+
name: t.name,
|
|
493
|
+
title: typeof t.title === "string" ? t.title : (t.annotations?.title ?? null),
|
|
494
|
+
category: categoryOf(t.name),
|
|
495
|
+
description: typeof t.description === "string" ? t.description : "",
|
|
496
|
+
risk: riskOf(t),
|
|
497
|
+
...callPolicy(t),
|
|
498
|
+
inputSchema:
|
|
499
|
+
t.inputSchema && typeof t.inputSchema === "object" ? t.inputSchema : { type: "object" },
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
cursor = r?.nextCursor;
|
|
503
|
+
if (!cursor) break;
|
|
504
|
+
}
|
|
505
|
+
tools = all.sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
506
|
+
return tools;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** Call one tool after checking its arguments; the result redacted. */
|
|
510
|
+
async function callTool(name, args) {
|
|
511
|
+
if (typeof name !== "string" || !TOOL_NAME.test(name))
|
|
512
|
+
throw new ToolError("unknown-tool", "no such tool");
|
|
513
|
+
const tool = (await listTools()).find((t) => t.name === name);
|
|
514
|
+
if (!tool) throw new ToolError("unknown-tool", "no such tool");
|
|
515
|
+
if (!tool.allowed) throw new ToolError("tool-not-allowed", tool.reason);
|
|
516
|
+
const input = args ?? {};
|
|
517
|
+
const confirms = confirmPaths(input);
|
|
518
|
+
if (confirms.length) {
|
|
519
|
+
throw new ToolError("invalid-arguments", "confirm is not accepted here", {
|
|
520
|
+
errors: confirms.map(
|
|
521
|
+
(p) => `${p} is not allowed: a tool's own preview is never skipped from here`,
|
|
522
|
+
),
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
const errors = schemaErrors(tool.inputSchema, input, "", { strict: true });
|
|
526
|
+
if (errors.length)
|
|
527
|
+
throw new ToolError(
|
|
528
|
+
"invalid-arguments",
|
|
529
|
+
"the arguments do not match the tool's inputSchema",
|
|
530
|
+
{ errors },
|
|
531
|
+
);
|
|
532
|
+
const r = await rpc("tools/call", { name, arguments: input }, CALL_TIMEOUT_MS);
|
|
533
|
+
return renderResult(tool, r);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function close() {
|
|
537
|
+
failAll(new ToolError("closed", "the client was closed"));
|
|
538
|
+
if (child && child.exitCode === null) child.kill("SIGTERM");
|
|
539
|
+
child = null;
|
|
540
|
+
ready = null;
|
|
541
|
+
tools = null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
listTools,
|
|
546
|
+
callTool,
|
|
547
|
+
close,
|
|
548
|
+
get started() {
|
|
549
|
+
return Boolean(child);
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/** A tools/call result as text, JSON text and images, every text redacted. */
|
|
555
|
+
export function renderResult(tool, result) {
|
|
556
|
+
const R = createRedactor();
|
|
557
|
+
const content = [];
|
|
558
|
+
for (const c of Array.isArray(result?.content) ? result.content : []) {
|
|
559
|
+
if (c?.type === "text") content.push({ kind: "text", text: R.clean(String(c.text ?? "")) });
|
|
560
|
+
else if (c?.type === "image" && IMAGE_TYPES.has(c.mimeType) && typeof c.data === "string") {
|
|
561
|
+
if (c.data.length * 0.75 <= MAX_IMAGE_BYTES)
|
|
562
|
+
content.push({ kind: "image", mime: c.mimeType, data: c.data });
|
|
563
|
+
else content.push({ kind: "text", text: "[image too large to show]" });
|
|
564
|
+
} else if (c?.type === "resource" && typeof c.resource?.text === "string") {
|
|
565
|
+
content.push({ kind: "text", text: R.clean(c.resource.text) });
|
|
566
|
+
} else if (c?.type)
|
|
567
|
+
content.push({ kind: "text", text: `[${String(c.type)} content not shown]` });
|
|
568
|
+
}
|
|
569
|
+
if (result?.structuredContent && typeof result.structuredContent === "object") {
|
|
570
|
+
content.push({
|
|
571
|
+
kind: "json",
|
|
572
|
+
text: R.clean(JSON.stringify(result.structuredContent, null, 2)),
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return {
|
|
576
|
+
contractVersion: CONTRACT_VERSION,
|
|
577
|
+
tool: tool.name,
|
|
578
|
+
risk: tool.risk,
|
|
579
|
+
isError: result?.isError === true,
|
|
580
|
+
content,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function main(argv) {
|
|
585
|
+
const [cmd, name, raw] = argv;
|
|
586
|
+
const client = createToolkitClient();
|
|
587
|
+
try {
|
|
588
|
+
let out;
|
|
589
|
+
if (cmd === "list" && !name) {
|
|
590
|
+
out = {
|
|
591
|
+
contractVersion: CONTRACT_VERSION,
|
|
592
|
+
version: activeToolkitVersion(),
|
|
593
|
+
tools: await client.listTools(),
|
|
594
|
+
};
|
|
595
|
+
} else if (cmd === "call" && name) {
|
|
596
|
+
let args = {};
|
|
597
|
+
if (raw !== undefined) {
|
|
598
|
+
try {
|
|
599
|
+
args = JSON.parse(raw);
|
|
600
|
+
} catch {
|
|
601
|
+
throw new ToolError("invalid-arguments", "the arguments are not JSON");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
out = await client.callTool(name, args);
|
|
605
|
+
} else {
|
|
606
|
+
process.stderr.write("usage: toolkit-tools.mjs list | call <name> '<json arguments>'\n");
|
|
607
|
+
return 2;
|
|
608
|
+
}
|
|
609
|
+
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
610
|
+
return 0;
|
|
611
|
+
} catch (err) {
|
|
612
|
+
if (!(err instanceof ToolError)) throw err;
|
|
613
|
+
process.stderr.write(`toolkit-tools: ${err.message}\n`);
|
|
614
|
+
for (const e of err.extra?.errors ?? []) process.stderr.write(` ${e}\n`);
|
|
615
|
+
return 1;
|
|
616
|
+
} finally {
|
|
617
|
+
client.close();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (invokedDirectly(import.meta.url)) {
|
|
622
|
+
runMain("toolkit-tools", async () => {
|
|
623
|
+
process.exitCode = await main(process.argv.slice(2));
|
|
624
|
+
});
|
|
625
|
+
}
|
|
@@ -139,9 +139,12 @@ const TOKEN_CHAR = /[A-Za-z0-9_\-./+=:@~%]/;
|
|
|
139
139
|
* `text` cut to `limit` characters, backed off to the start of a token that
|
|
140
140
|
* the cut would split, so no half-credential survives on the boundary.
|
|
141
141
|
*/
|
|
142
|
+
// The " ..." ending counts toward the limit, so a cut text is never longer
|
|
143
|
+
// than the field that holds it.
|
|
142
144
|
function cut(text, limit) {
|
|
143
145
|
if (text.length <= limit) return text;
|
|
144
|
-
|
|
146
|
+
if (limit < 5) return text.slice(0, limit);
|
|
147
|
+
let end = limit - 4;
|
|
145
148
|
while (end > 0 && TOKEN_CHAR.test(text[end]) && TOKEN_CHAR.test(text[end - 1])) end--;
|
|
146
149
|
return `${text.slice(0, end).trimEnd()} ...`;
|
|
147
150
|
}
|
|
@@ -997,6 +997,7 @@ export async function main() {
|
|
|
997
997
|
rmIfExists(join(CLAUDE, "multi-agent-refs"));
|
|
998
998
|
rmIfExists(join(CLAUDE, ".pipeline-swap"));
|
|
999
999
|
rmIfExists(join(CLAUDE, "schemas"));
|
|
1000
|
+
rmIfExists(join(CLAUDE, "office"));
|
|
1000
1001
|
if (!rmManifestFiles(CLAUDE, KEEP_IN_LIB)) {
|
|
1001
1002
|
// An install that predates the manifest wiped lib/ and templates/ on
|
|
1002
1003
|
// every run, so their shipped names are the pipeline's. The top-level
|
|
@@ -1053,6 +1054,7 @@ export async function main() {
|
|
|
1053
1054
|
rmIfExists(join(COP, "scripts"));
|
|
1054
1055
|
// Pipeline-managed trees the installer lays down alongside scripts/.
|
|
1055
1056
|
rmIfExists(join(COP, "schemas"));
|
|
1057
|
+
rmIfExists(join(COP, "office"));
|
|
1056
1058
|
rmDirExcept(join(COP, "lib"), KEEP_IN_LIB);
|
|
1057
1059
|
rmIfExists(join(COP, ".pipeline-version"));
|
|
1058
1060
|
const skills = join(COP, "skills");
|
|
@@ -1164,6 +1166,7 @@ export async function main() {
|
|
|
1164
1166
|
rmIfExists(join(CODEX, "scripts"));
|
|
1165
1167
|
rmDirExcept(join(CODEX, "lib"), KEEP_IN_LIB);
|
|
1166
1168
|
rmIfExists(join(CODEX, "schemas"));
|
|
1169
|
+
rmIfExists(join(CODEX, "office"));
|
|
1167
1170
|
rmIfExists(join(CODEX, "rules"));
|
|
1168
1171
|
rmIfExists(join(CODEX, ".pipeline-version"));
|
|
1169
1172
|
|