@zq-silk/yui 0.0.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.
Files changed (95) hide show
  1. package/ARCHITECTURE.md +141 -0
  2. package/LICENSE +21 -0
  3. package/README.md +211 -0
  4. package/dist/agent/adapterCatalog.js +10 -0
  5. package/dist/agent/agent.js +89 -0
  6. package/dist/agent/agentRegistry.js +10 -0
  7. package/dist/agent/argumentPolicy.js +80 -0
  8. package/dist/brief/taskBrief.js +37 -0
  9. package/dist/cli/commandCatalog.js +647 -0
  10. package/dist/cli/completion.js +111 -0
  11. package/dist/cli/completionWizard.js +143 -0
  12. package/dist/cli/dynamicCompletion.js +48 -0
  13. package/dist/cli/helpRenderer.js +32 -0
  14. package/dist/cli/interactionCandidates.js +139 -0
  15. package/dist/cli/interactionPolicy.js +389 -0
  16. package/dist/cli/interactiveSelection.js +185 -0
  17. package/dist/cli/invocationRouter.js +51 -0
  18. package/dist/cli/roleOptionCatalog.js +67 -0
  19. package/dist/cli/roleWizard.js +546 -0
  20. package/dist/cli/selectionPorts.js +1 -0
  21. package/dist/cli/updateCommand.js +22 -0
  22. package/dist/cli.js +402 -0
  23. package/dist/commands/agentCommands.js +196 -0
  24. package/dist/commands/globalRoleCommands.js +367 -0
  25. package/dist/commands/jobCommands.js +100 -0
  26. package/dist/commands/operatorCommands.js +38 -0
  27. package/dist/commands/repositoryCommands.js +86 -0
  28. package/dist/commands/roleConfiguration.js +201 -0
  29. package/dist/commands/taskCommands.js +1344 -0
  30. package/dist/commands/taskContextCommand.js +215 -0
  31. package/dist/commands/taskInputCommands.js +423 -0
  32. package/dist/commands/taskRoleRuntimeStatus.js +152 -0
  33. package/dist/completion/completionInstaller.js +168 -0
  34. package/dist/completion/completionPort.js +1 -0
  35. package/dist/completion/completionState.js +137 -0
  36. package/dist/completion/completionWizard.js +125 -0
  37. package/dist/completion/fileCompletionManager.js +51 -0
  38. package/dist/config/yuiConfig.js +17 -0
  39. package/dist/context/dispatchContext.js +74 -0
  40. package/dist/controller/clientRuntime.js +215 -0
  41. package/dist/controller/controller.js +158 -0
  42. package/dist/controller/controllerMain.js +37 -0
  43. package/dist/controller/fileSchedulerStoreAdapter.js +322 -0
  44. package/dist/controller/runtime.js +31 -0
  45. package/dist/controller/sessionNotify.js +136 -0
  46. package/dist/core/controllerClient.js +127 -0
  47. package/dist/core/controllerServer.js +269 -0
  48. package/dist/core/protocol.js +169 -0
  49. package/dist/decision/decision.js +42 -0
  50. package/dist/doctor/doctor.js +229 -0
  51. package/dist/errors/cliError.js +38 -0
  52. package/dist/event/taskEvent.js +44 -0
  53. package/dist/executor/agentAdapter.js +338 -0
  54. package/dist/executor/agentExecutor.js +144 -0
  55. package/dist/executor/executorRegistry.js +101 -0
  56. package/dist/executor/fileRoleLaunchPlanner.js +156 -0
  57. package/dist/executor/launchPlan.js +16 -0
  58. package/dist/input/inputRequest.js +326 -0
  59. package/dist/message/message.js +69 -0
  60. package/dist/milestone/milestone.js +27 -0
  61. package/dist/operator/operatorContext.js +66 -0
  62. package/dist/output/rolePresentation.js +82 -0
  63. package/dist/output/table.js +77 -0
  64. package/dist/output/terminal.js +198 -0
  65. package/dist/repository/gitWorkspace.js +210 -0
  66. package/dist/repository/repository.js +55 -0
  67. package/dist/repository/taskWorkspacePreparer.js +256 -0
  68. package/dist/role/role.js +246 -0
  69. package/dist/role/systemRoles.js +20 -0
  70. package/dist/run/agentRun.js +102 -0
  71. package/dist/scheduler/activeRoleRunDelivery.js +94 -0
  72. package/dist/scheduler/archivedTaskRuntime.js +12 -0
  73. package/dist/scheduler/leaderFailure.js +18 -0
  74. package/dist/scheduler/leaderWakeupProcessor.js +143 -0
  75. package/dist/scheduler/operatorInputNotificationProcessor.js +85 -0
  76. package/dist/scheduler/operatorNotification.js +17 -0
  77. package/dist/scheduler/pendingWakeup.js +33 -0
  78. package/dist/scheduler/ports.js +1 -0
  79. package/dist/scheduler/roleRunLiveness.js +41 -0
  80. package/dist/scheduler/wakeupQueue.js +13 -0
  81. package/dist/setup/setupCommand.js +317 -0
  82. package/dist/storage/durableFile.js +38 -0
  83. package/dist/storage/storageSchema.js +259 -0
  84. package/dist/storage/taskStore.js +1032 -0
  85. package/dist/task/task.js +216 -0
  86. package/dist/tmux/commandExecutor.js +69 -0
  87. package/dist/tmux/terminalHandoff.js +17 -0
  88. package/dist/tmux/tmuxManager.js +408 -0
  89. package/dist/workItem/workItem.js +45 -0
  90. package/dist/worktree/roleWorkspace.js +62 -0
  91. package/i18n/README.zh-CN.md +205 -0
  92. package/package.json +47 -0
  93. package/skills/yui-leader/SKILL.md +72 -0
  94. package/skills/yui-operator/SKILL.md +57 -0
  95. package/skills/yui-worker/SKILL.md +31 -0
@@ -0,0 +1,229 @@
1
+ import { accessSync, constants, existsSync, lstatSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { configuredAgentToDefinition } from "../agent/agent.js";
4
+ import { inspectAgentCapabilities } from "../executor/agentAdapter.js";
5
+ import { usageError } from "../errors/cliError.js";
6
+ import { defaultTableWidth, renderTable } from "../output/table.js";
7
+ import { FileTaskStore, resolveYuiHome, STORAGE_STATE_FILE } from "../storage/taskStore.js";
8
+ import { inspectStorageSchema } from "../storage/storageSchema.js";
9
+ import { CommandExecutionError } from "../tmux/commandExecutor.js";
10
+ /** Runs the read-only FileTaskStore diagnostics used by `yui doctor`. */
11
+ export function runDoctorCommand(args, env, executor) {
12
+ if (args.length !== 0)
13
+ throw usageError("Doctor usage: yui doctor");
14
+ return renderDoctor(getDoctorChecks(env, executor));
15
+ }
16
+ export function getDoctorChecks(env, executor) {
17
+ const home = resolveYuiHome(env);
18
+ const homeCheck = checkHome(home);
19
+ const schema = readSchema(home);
20
+ const schemaCheck = checkSchema(schema);
21
+ const storage = inspectState(home, homeCheck, schema);
22
+ return [
23
+ homeCheck,
24
+ schemaCheck,
25
+ storage.check,
26
+ checkExecutable("git", env.YUI_GIT_BIN ?? "git", ["--version"], executor),
27
+ checkExecutable("tmux", env.YUI_TMUX_BIN ?? "tmux", ["-V"], executor),
28
+ ...storage.agents.flatMap((agent) => checkAgent(agent, executor))
29
+ ];
30
+ }
31
+ export function renderDoctor(checks) {
32
+ return `Yui doctor\n${renderTable("Checks", [
33
+ { header: "Check", minWidth: 8, maxWidth: 28 },
34
+ { header: "Status", minWidth: 7, maxWidth: 11 },
35
+ { header: "Detail", minWidth: 12, maxWidth: 88 }
36
+ ], checks.map((check) => [check.name, check.status, check.detail]), defaultTableWidth())}\n`;
37
+ }
38
+ function checkHome(home) {
39
+ try {
40
+ const metadata = lstatSync(home);
41
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
42
+ return {
43
+ name: "yui home",
44
+ status: "invalid",
45
+ detail: "YUI_HOME must be a real directory."
46
+ };
47
+ }
48
+ accessSync(home, constants.R_OK);
49
+ return { name: "yui home", status: "ok", detail: home };
50
+ }
51
+ catch (error) {
52
+ if (systemCode(error) === "ENOENT") {
53
+ return { name: "yui home", status: "missing", detail: "run yui setup" };
54
+ }
55
+ return { name: "yui home", status: "invalid", detail: errorMessage(error) };
56
+ }
57
+ }
58
+ function readSchema(home) {
59
+ try {
60
+ return inspectStorageSchema(home);
61
+ }
62
+ catch (error) {
63
+ return { status: "read-error", detail: errorMessage(error) };
64
+ }
65
+ }
66
+ function checkSchema(state) {
67
+ switch (state.status) {
68
+ case "uninitialized":
69
+ return { name: "storage schema", status: "missing", detail: "run yui setup" };
70
+ case "current":
71
+ return {
72
+ name: "storage schema",
73
+ status: "ok",
74
+ detail: `current=${state.currentVersion} latest=${state.latestVersion}`
75
+ };
76
+ case "unsupported":
77
+ return {
78
+ name: "storage schema",
79
+ status: "unsupported",
80
+ detail: `current=${state.currentVersion} latest=${state.latestVersion} direction=${state.direction}`
81
+ };
82
+ case "invalid":
83
+ return { name: "storage schema", status: "invalid", detail: state.detail };
84
+ case "read-error":
85
+ return { name: "storage schema", status: "invalid", detail: state.detail };
86
+ }
87
+ }
88
+ function inspectState(home, homeCheck, schema) {
89
+ if (homeCheck.status !== "ok") {
90
+ return blockedStorage(homeCheck.status, homeCheck.detail);
91
+ }
92
+ if (schema.status === "uninitialized") {
93
+ return blockedStorage("missing", "run yui setup");
94
+ }
95
+ if (schema.status === "unsupported") {
96
+ return blockedStorage("unsupported", `current=${schema.currentVersion} latest=${schema.latestVersion}`);
97
+ }
98
+ if (schema.status === "invalid")
99
+ return blockedStorage("invalid", schema.detail);
100
+ if (schema.status === "read-error")
101
+ return blockedStorage("invalid", schema.detail);
102
+ const statePath = join(home, STORAGE_STATE_FILE);
103
+ if (!existsSync(statePath))
104
+ return blockedStorage("missing", "run yui setup");
105
+ try {
106
+ const metadata = lstatSync(statePath);
107
+ if (!metadata.isFile() || metadata.isSymbolicLink()) {
108
+ return blockedStorage("invalid", `${STORAGE_STATE_FILE} must be a regular file.`);
109
+ }
110
+ accessSync(statePath, constants.R_OK);
111
+ const store = new FileTaskStore(home);
112
+ const config = store.getConfig();
113
+ const agents = store.listConfiguredAgents();
114
+ const tasks = store.listTasks();
115
+ const globalRoles = store.listGlobalRoles();
116
+ const roleCount = tasks.reduce((count, task) => count + store.listRoles(task.id).length, 0);
117
+ return {
118
+ check: {
119
+ name: "storage state",
120
+ status: "ok",
121
+ detail: `readable agents=${agents.length} tasks=${tasks.length} roles=${roleCount} globalRoles=${globalRoles.length} defaultAgent=${config.defaultAgent ?? "none"}`
122
+ },
123
+ agents
124
+ };
125
+ }
126
+ catch (error) {
127
+ return blockedStorage("invalid", errorMessage(error));
128
+ }
129
+ }
130
+ function blockedStorage(status, detail) {
131
+ return {
132
+ check: { name: "storage state", status, detail },
133
+ agents: []
134
+ };
135
+ }
136
+ function checkExecutable(name, command, args, executor) {
137
+ try {
138
+ const output = firstLine(executor.run(command, args));
139
+ return {
140
+ name,
141
+ status: "ok",
142
+ detail: output.length === 0 ? command : `${command}: ${output}`
143
+ };
144
+ }
145
+ catch (error) {
146
+ return {
147
+ name,
148
+ status: isMissingCommand(error) ? "missing" : "invalid",
149
+ detail: `${command}: ${commandFailure(error)}`
150
+ };
151
+ }
152
+ }
153
+ function checkAgent(agent, executor) {
154
+ let snapshot;
155
+ try {
156
+ snapshot = inspectAgentCapabilities(configuredAgentToDefinition(agent), {
157
+ run: (command, args) => runAgentProbe(executor, command, args)
158
+ });
159
+ }
160
+ catch (error) {
161
+ return [{
162
+ name: `agent:${agent.id}`,
163
+ status: "invalid",
164
+ detail: `${agent.command}: ${errorMessage(error)}`
165
+ }];
166
+ }
167
+ const installation = snapshot.installation;
168
+ const status = installation.status === "installed"
169
+ ? "ok"
170
+ : installation.status === "missing"
171
+ ? "missing"
172
+ : installation.status === "unsupported-version" ? "unsupported" : "invalid";
173
+ const commandDetail = [
174
+ `command=${installation.command}`,
175
+ `adapter=${snapshot.adapterId}`,
176
+ ...(installation.version === undefined ? [] : [`version=${installation.version}`]),
177
+ ...(installation.reason === undefined ? [] : [`reason=${installation.reason}`])
178
+ ].join(" ");
179
+ const available = snapshot.fields.filter((field) => field.status === "available").length;
180
+ const degraded = snapshot.fields.filter((field) => field.status === "degraded").length;
181
+ const unavailable = snapshot.fields.filter((field) => field.status === "unavailable").length;
182
+ return [
183
+ { name: `agent:${agent.id}:command`, status, detail: commandDetail },
184
+ {
185
+ name: `agent:${agent.id}:capability`,
186
+ status,
187
+ detail: `start resume interrupt nativeSession=${snapshot.lifecycle.nativeSessionDiscovery} fields=${available}/${degraded}/${unavailable}`
188
+ }
189
+ ];
190
+ }
191
+ function runAgentProbe(executor, command, args) {
192
+ try {
193
+ return { status: 0, stdout: executor.run(command, [...args]), stderr: "" };
194
+ }
195
+ catch (error) {
196
+ const missing = isMissingCommand(error);
197
+ const probeError = Object.assign(new Error(errorMessage(error)), {
198
+ ...(missing ? { code: "ENOENT" } : {})
199
+ });
200
+ return {
201
+ status: error instanceof CommandExecutionError ? error.exitStatus ?? null : null,
202
+ stdout: "",
203
+ stderr: error instanceof CommandExecutionError ? error.stderr : "",
204
+ error: probeError
205
+ };
206
+ }
207
+ }
208
+ function isMissingCommand(error) {
209
+ return error instanceof CommandExecutionError
210
+ ? error.code === "COMMAND_NOT_FOUND"
211
+ : systemCode(error) === "ENOENT";
212
+ }
213
+ function commandFailure(error) {
214
+ if (error instanceof CommandExecutionError) {
215
+ return error.stderr.trim() || error.message;
216
+ }
217
+ return errorMessage(error);
218
+ }
219
+ function firstLine(output) {
220
+ return output.trim().split(/\r?\n/, 1)[0] ?? "";
221
+ }
222
+ function systemCode(error) {
223
+ return error instanceof Error && "code" in error && typeof error.code === "string"
224
+ ? error.code
225
+ : undefined;
226
+ }
227
+ function errorMessage(error) {
228
+ return error instanceof Error ? error.message : String(error);
229
+ }
@@ -0,0 +1,38 @@
1
+ const EXIT_CODES = {
2
+ USAGE_ERROR: 2,
3
+ TASK_NOT_FOUND: 3,
4
+ ROLE_NOT_FOUND: 3,
5
+ AGENT_NOT_FOUND: 3,
6
+ DATA_ERROR: 4,
7
+ RUNTIME_ERROR: 5
8
+ };
9
+ export class CliError extends Error {
10
+ code;
11
+ helpText;
12
+ exitCode;
13
+ constructor(code, message, helpText) {
14
+ super(message);
15
+ this.code = code;
16
+ this.helpText = helpText;
17
+ this.name = "CliError";
18
+ this.exitCode = EXIT_CODES[code];
19
+ }
20
+ }
21
+ export function usageError(message, helpText) {
22
+ return new CliError("USAGE_ERROR", message, helpText);
23
+ }
24
+ export function taskNotFound(id) {
25
+ return new CliError("TASK_NOT_FOUND", `Task not found: ${id}`);
26
+ }
27
+ export function roleNotFound(name) {
28
+ return new CliError("ROLE_NOT_FOUND", `Role not found: ${name}`);
29
+ }
30
+ export function agentNotFound(id) {
31
+ return new CliError("AGENT_NOT_FOUND", `Agent not found: ${id}`);
32
+ }
33
+ export function dataError(message) {
34
+ return new CliError("DATA_ERROR", message);
35
+ }
36
+ export function runtimeError(message) {
37
+ return new CliError("RUNTIME_ERROR", message);
38
+ }
@@ -0,0 +1,44 @@
1
+ export function createTaskEvent(id, type, payload, now) {
2
+ return {
3
+ schemaVersion: 1,
4
+ id: requireText(id, "Task event id"),
5
+ type: requireText(type, "Task event type"),
6
+ payload: normalizePayload(payload),
7
+ createdAt: now.toISOString()
8
+ };
9
+ }
10
+ /**
11
+ * Builds the next Event history without mutating or replacing an existing entry.
12
+ * Persistence adapters should enforce the same unique-id rule when appending.
13
+ */
14
+ export function appendTaskEvent(history, event) {
15
+ if (history.some((existing) => existing.id === event.id)) {
16
+ throw new Error(`Task event already exists: ${event.id}.`);
17
+ }
18
+ return [...history, { ...event, payload: { ...event.payload } }];
19
+ }
20
+ function normalizePayload(payload) {
21
+ const normalized = {};
22
+ for (const [key, value] of Object.entries(payload)) {
23
+ const normalizedKey = requireText(key, "Task event payload key");
24
+ if (["__proto__", "prototype", "constructor"].includes(normalizedKey)) {
25
+ throw new Error(`Task event payload key is invalid: ${normalizedKey}.`);
26
+ }
27
+ if (Object.hasOwn(normalized, normalizedKey)) {
28
+ throw new Error(`Task event payload key is duplicated: ${normalizedKey}.`);
29
+ }
30
+ if (typeof value !== "string" || value.includes("\0")) {
31
+ throw new Error(`Task event payload value is invalid: ${normalizedKey}.`);
32
+ }
33
+ normalized[normalizedKey] = value;
34
+ }
35
+ return normalized;
36
+ }
37
+ function requireText(value, label) {
38
+ if (typeof value !== "string" || value.includes("\0"))
39
+ throw new Error(`${label} is invalid.`);
40
+ const normalized = value.trim();
41
+ if (normalized.length === 0)
42
+ throw new Error(`${label} is required.`);
43
+ return normalized;
44
+ }
@@ -0,0 +1,338 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { realpathSync, statSync } from "node:fs";
3
+ import { isAbsolute, resolve } from "node:path";
4
+ import { supportedAgentAdapterIds } from "../agent/adapterCatalog.js";
5
+ import { ownedArgumentsForAdapter, validateAgentAdvancedArguments, validateAgentBaseArguments } from "../agent/argumentPolicy.js";
6
+ const SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
7
+ const APPROVALS = ["untrusted", "on-request", "never"];
8
+ const PROBE_TIMEOUT_MS = 2_000;
9
+ const PROBE_MAX_BYTES = 1024 * 1024;
10
+ class BaseAdapter {
11
+ validateConfig(input) {
12
+ if (input.agent.adapterId !== this.id || input.config.adapterId !== this.id) {
13
+ throw new Error(`Agent adapter identity mismatch: expected ${this.id}.`);
14
+ }
15
+ validateAgentBaseArguments(this.id, input.agent.baseArgs);
16
+ this.validateStructured(input.config);
17
+ }
18
+ compileNew(input) {
19
+ this.validateConfig(input);
20
+ const config = this.canonicalizeConfig(input.config);
21
+ return {
22
+ argv: [...input.agent.baseArgs, ...this.structuredArgs(config), ...(config.advanced?.rawArgs ?? [])],
23
+ sessionStrategy: this.capabilities.nativeSessionDiscovery === "runtime"
24
+ ? "runtime-discovery"
25
+ : "preallocated"
26
+ };
27
+ }
28
+ canonicalizeConfig(config) {
29
+ this.validateStructured(config);
30
+ const directories = config.additionalDirectories === undefined
31
+ ? undefined
32
+ : canonicalDirectories(config.additionalDirectories);
33
+ return cloneConfig(config, directories);
34
+ }
35
+ reservedArguments() {
36
+ return ownedArgumentsForAdapter(this.id);
37
+ }
38
+ }
39
+ class CodexAdapter extends BaseAdapter {
40
+ id = "codex";
41
+ label = "Codex";
42
+ supportedVersion = "0.144.1";
43
+ capabilities = { recover: true, interrupt: true, nativeSessionDiscovery: "runtime" };
44
+ validateStructured(config) {
45
+ exact(config, ["adapterId", "model", "effort", "permission", "search", "profile",
46
+ "additionalDirectories", "advanced"], "Codex Agent config");
47
+ if (config.adapterId !== "codex")
48
+ throw new Error("Codex Agent config adapter is invalid.");
49
+ optionalText(config.model, "Codex model");
50
+ optionalText(config.effort, "Codex effort");
51
+ optionalText(config.profile, "Codex profile");
52
+ if (config.search !== undefined && typeof config.search !== "boolean") {
53
+ throw new Error("Codex search must be boolean.");
54
+ }
55
+ validatePaths(config.additionalDirectories, "Codex additional directory");
56
+ if (config.permission !== undefined) {
57
+ exact(config.permission, ["sandbox", "approval"], "Codex permission config");
58
+ if (config.permission.sandbox !== undefined && !SANDBOXES.includes(config.permission.sandbox)) {
59
+ throw new Error("Codex sandbox is invalid.");
60
+ }
61
+ if (config.permission.approval !== undefined && !APPROVALS.includes(config.permission.approval)) {
62
+ throw new Error("Codex approval is invalid.");
63
+ }
64
+ }
65
+ advanced(this.id, config.advanced);
66
+ }
67
+ structuredArgs(config) {
68
+ return [
69
+ // Yui launches Codex in a detached tmux window and waits for the
70
+ // composer before delivering work. The startup updater is itself an
71
+ // interactive prompt, so leaving it enabled can consume the first
72
+ // automated delivery as an update-menu answer.
73
+ "--config", "check_for_update_on_startup=false",
74
+ ...(config.model === undefined ? [] : ["--model", config.model]),
75
+ ...(config.effort === undefined ? [] : ["--config", `model_reasoning_effort=\"${config.effort}\"`]),
76
+ ...(config.permission?.sandbox === undefined ? [] : ["--sandbox", config.permission.sandbox]),
77
+ ...(config.permission?.approval === undefined ? [] : ["--ask-for-approval", config.permission.approval]),
78
+ ...(config.search === true ? ["--search"] : []),
79
+ ...(config.profile === undefined ? [] : ["--profile", config.profile]),
80
+ ...(config.additionalDirectories ?? []).flatMap((path) => ["--add-dir", path])
81
+ ];
82
+ }
83
+ compileResume(input) {
84
+ const launch = this.compileNew(input);
85
+ return { ...launch, argv: [...launch.argv, "resume", nativeId(input.nativeSessionId)] };
86
+ }
87
+ }
88
+ class ClaudeAdapter extends BaseAdapter {
89
+ id = "claude";
90
+ label = "Claude";
91
+ supportedVersion = "2.1.207";
92
+ capabilities = { recover: true, interrupt: true, nativeSessionDiscovery: "preallocated" };
93
+ validateStructured(config) {
94
+ exact(config, ["adapterId", "model", "effort", "permission", "additionalDirectories",
95
+ "settingsFile", "settingsSources", "advanced"], "Claude Agent config");
96
+ if (config.adapterId !== "claude")
97
+ throw new Error("Claude Agent config adapter is invalid.");
98
+ optionalText(config.model, "Claude model");
99
+ optionalText(config.effort, "Claude effort");
100
+ validatePaths(config.additionalDirectories, "Claude additional directory");
101
+ if (config.settingsFile !== undefined)
102
+ absolutePath(config.settingsFile, "Claude settings file");
103
+ optionalTexts(config.settingsSources, "Claude settings source");
104
+ if (config.settingsSources !== undefined && new Set(config.settingsSources).size !== config.settingsSources.length) {
105
+ throw new Error("Claude settings sources contain duplicates.");
106
+ }
107
+ if (config.permission !== undefined) {
108
+ exact(config.permission, ["mode", "allowedTools", "disallowedTools"], "Claude permission config");
109
+ optionalText(config.permission.mode, "Claude permission mode");
110
+ optionalTexts(config.permission.allowedTools, "Claude allowed tool");
111
+ optionalTexts(config.permission.disallowedTools, "Claude disallowed tool");
112
+ for (const tool of [...(config.permission.allowedTools ?? []), ...(config.permission.disallowedTools ?? [])]) {
113
+ if (/(?:api[-_]?key|token|secret|password|credential|authorization|Bearer\s+\S+|sk-[\w-]{8,})/i.test(tool)) {
114
+ throw new Error("Claude tool expressions cannot contain secret-bearing literals.");
115
+ }
116
+ }
117
+ }
118
+ advanced(this.id, config.advanced);
119
+ }
120
+ structuredArgs(config) {
121
+ return [
122
+ ...(config.model === undefined ? [] : ["--model", config.model]),
123
+ ...(config.effort === undefined ? [] : ["--effort", config.effort]),
124
+ ...(config.permission?.mode === undefined ? [] : ["--permission-mode", config.permission.mode]),
125
+ ...(config.permission?.allowedTools === undefined ? [] : ["--allowed-tools", ...config.permission.allowedTools]),
126
+ ...(config.permission?.disallowedTools === undefined ? [] : ["--disallowed-tools", ...config.permission.disallowedTools]),
127
+ ...(config.additionalDirectories ?? []).flatMap((path) => ["--add-dir", path]),
128
+ ...(config.settingsFile === undefined ? [] : ["--settings", config.settingsFile]),
129
+ ...(config.settingsSources === undefined ? [] : ["--setting-sources", config.settingsSources.join(",")])
130
+ ];
131
+ }
132
+ compileResume(input) {
133
+ const launch = this.compileNew(input);
134
+ return { ...launch, argv: [...launch.argv, "--resume", nativeId(input.nativeSessionId)] };
135
+ }
136
+ }
137
+ const ADAPTERS = {
138
+ codex: new CodexAdapter(), claude: new ClaudeAdapter()
139
+ };
140
+ export { supportedAgentAdapterIds };
141
+ export function findAgentAdapter(id) {
142
+ return id === "codex" || id === "claude" ? ADAPTERS[id] : null;
143
+ }
144
+ export function resolveAgentAdapter(id) {
145
+ const adapter = findAgentAdapter(id);
146
+ if (adapter === null)
147
+ throw new Error(`Agent adapter is unsupported: ${id}.`);
148
+ return adapter;
149
+ }
150
+ export function inspectAgentCapabilities(agent, optionsOrNow = {}) {
151
+ const options = optionsOrNow instanceof Date ? { now: optionsOrNow } : optionsOrNow;
152
+ const now = options.now ?? new Date();
153
+ const run = options.run ?? runProbe;
154
+ const at = now.toISOString();
155
+ const adapter = resolveAgentAdapter(agent.adapterId);
156
+ validateAgentBaseArguments(agent.adapterId, agent.baseArgs);
157
+ const versionRun = run(agent.command, ["--version"]);
158
+ const failure = failed(versionRun);
159
+ if (failure !== undefined) {
160
+ const missing = versionRun.error?.code === "ENOENT";
161
+ return snapshot(agent, adapter, {
162
+ status: missing ? "missing" : "probe-failed", command: agent.command,
163
+ reason: missing ? "Agent command was not found." : failure, probedAt: at
164
+ }, baseline(agent.adapterId), at);
165
+ }
166
+ const version = /(?:^|\D)(\d+\.\d+\.\d+)(?:\D|$)/m
167
+ .exec(output(versionRun.stdout, versionRun.stderr))?.[1];
168
+ if (version === undefined) {
169
+ return snapshot(agent, adapter, {
170
+ status: "probe-failed", command: agent.command,
171
+ reason: "Agent version probe did not return a semantic version.", probedAt: at
172
+ }, baseline(agent.adapterId), at);
173
+ }
174
+ const supported = supports(version, adapter.supportedVersion);
175
+ let fields = baseline(agent.adapterId);
176
+ const warnings = supported ? [] : [`Installed version ${version} is not supported by adapter ${adapter.id}.`];
177
+ if (supported) {
178
+ const help = run(agent.command, ["--help"]);
179
+ if (failed(help) === undefined)
180
+ fields = fromHelp(agent.adapterId, output(help.stdout, help.stderr));
181
+ }
182
+ return snapshot(agent, adapter, {
183
+ status: supported ? "installed" : "unsupported-version", command: agent.command, version,
184
+ ...(supported ? {} : { reason: `Supported version line starts at ${adapter.supportedVersion}.` }),
185
+ probedAt: at
186
+ }, fields, at, warnings);
187
+ }
188
+ function baseline(id) {
189
+ if (id === "codex")
190
+ return [
191
+ field("model", "enum", "degraded", true), field("effort", "enum", "unavailable", true),
192
+ field("permission.sandbox", "enum", "available", false, SANDBOXES),
193
+ field("permission.approval", "enum", "available", false, APPROVALS),
194
+ field("profile", "string", "available", true), field("search", "boolean", "available", false, ["true"]),
195
+ field("additionalDirectories", "path-list", "available", true)
196
+ ];
197
+ return [
198
+ field("model", "enum", "degraded", true, ["fable", "opus", "sonnet"]),
199
+ field("effort", "enum", "unavailable", true), field("permission.mode", "enum", "unavailable", true),
200
+ field("permission.allowedTools", "string-list", "available", true),
201
+ field("permission.disallowedTools", "string-list", "available", true),
202
+ field("additionalDirectories", "path-list", "available", true), field("settingsFile", "path", "available", true),
203
+ field("settingsSources", "string-list", "degraded", false, ["user", "project", "local"])
204
+ ];
205
+ }
206
+ function fromHelp(id, help) {
207
+ const fields = baseline(id);
208
+ const replacements = id === "codex"
209
+ ? [choiceField("permission.sandbox", help, "--sandbox", SANDBOXES),
210
+ choiceField("permission.approval", help, "--ask-for-approval", APPROVALS)]
211
+ : [choiceField("model", help, "--model", ["fable", "opus", "sonnet"], true),
212
+ choiceField("effort", help, "--effort", [], true),
213
+ choiceField("permission.mode", help, "--permission-mode", [], true),
214
+ choiceField("settingsSources", help, "--setting-sources", ["user", "project", "local"])];
215
+ const byKey = new Map(replacements.map((value) => [value.key, value]));
216
+ return fields.map((value) => byKey.get(value.key) ?? value);
217
+ }
218
+ function choiceField(key, help, flag, fallback, custom = false) {
219
+ const choices = helpChoices(help, flag);
220
+ return field(key, key === "settingsSources" ? "string-list" : "enum", choices.length > 0 ? "available" : fallback.length > 0 ? "degraded" : "unavailable", custom, choices.length > 0 ? choices : fallback);
221
+ }
222
+ function field(key, kind, status, allowCustom, choices) {
223
+ return { key, kind, status, allowCustom, ...(choices === undefined ? {} : { choices: [...choices] }) };
224
+ }
225
+ function helpChoices(help, flag) {
226
+ const lines = help.replace(/\r\n/g, "\n").split("\n");
227
+ const start = lines.findIndex((line) => line.includes(flag));
228
+ if (start < 0)
229
+ return [];
230
+ let end = start + 1;
231
+ while (end < lines.length && !/^\s{2,}(?:-[A-Za-z](?:,\s*)?|--)[\w-]/.test(lines[end]))
232
+ end += 1;
233
+ const body = /(?:\[possible values:\s*([^\]]+)\]|\(choices:\s*([^)]*)\)|\(([^)]*)\))/i
234
+ .exec(lines.slice(start, end).join("\n"));
235
+ return [...new Set((body?.[1] ?? body?.[2] ?? body?.[3] ?? "").replace(/["']/g, "").split(",")
236
+ .map((value) => value.trim().replace(/\.$/, "")).filter((value) => /^[\w.+-]+$/.test(value)))];
237
+ }
238
+ function snapshot(agent, adapter, installation, fields, at, warnings = []) {
239
+ return { schemaVersion: 1, agentId: agent.id, adapterId: agent.adapterId, installation,
240
+ lifecycle: { start: true, resume: true, nativeSessionDiscovery: adapter.capabilities.nativeSessionDiscovery,
241
+ interrupt: true }, fields, warnings, refreshedAt: at };
242
+ }
243
+ function runProbe(command, args) {
244
+ const result = spawnSync(command, [...args], { encoding: "utf8", shell: false, timeout: PROBE_TIMEOUT_MS,
245
+ maxBuffer: PROBE_MAX_BYTES, stdio: ["ignore", "pipe", "pipe"] });
246
+ return { status: result.status, stdout: String(result.stdout ?? ""), stderr: String(result.stderr ?? ""),
247
+ ...(result.error === undefined ? {} : { error: result.error }) };
248
+ }
249
+ function failed(result) {
250
+ if (result.error !== undefined)
251
+ return result.error.code === "ETIMEDOUT"
252
+ ? `Agent probe timed out after ${PROBE_TIMEOUT_MS} ms.` : "Agent probe failed to start.";
253
+ return result.status === 0 ? undefined : `Agent probe exited with status ${result.status ?? "unknown"}.`;
254
+ }
255
+ function output(stdout, stderr) {
256
+ const value = `${stdout}\n${stderr}`;
257
+ if (Buffer.byteLength(value, "utf8") > PROBE_MAX_BYTES)
258
+ throw new Error("Agent probe output exceeded 1 MiB.");
259
+ return value;
260
+ }
261
+ function supports(version, baseline) {
262
+ const left = version.split(".").map(Number), right = baseline.split(".").map(Number);
263
+ return left[0] === right[0] && left[1] === right[1] && left[2] >= right[2];
264
+ }
265
+ function cloneConfig(config, paths) {
266
+ const advancedConfig = config.advanced?.rawArgs === undefined ? config.advanced : { rawArgs: [...config.advanced.rawArgs] };
267
+ if (config.adapterId === "codex")
268
+ return { ...config,
269
+ ...(config.permission === undefined ? {} : { permission: { ...config.permission } }),
270
+ ...(paths === undefined ? {} : { additionalDirectories: [...paths] }),
271
+ ...(advancedConfig === undefined ? {} : { advanced: advancedConfig }) };
272
+ return { ...config,
273
+ ...(config.permission === undefined ? {} : { permission: { ...config.permission,
274
+ ...(config.permission.allowedTools === undefined ? {} : { allowedTools: [...config.permission.allowedTools] }),
275
+ ...(config.permission.disallowedTools === undefined ? {} : { disallowedTools: [...config.permission.disallowedTools] }) } }),
276
+ ...(paths === undefined ? {} : { additionalDirectories: [...paths] }),
277
+ ...(config.settingsSources === undefined ? {} : { settingsSources: [...config.settingsSources] }),
278
+ ...(advancedConfig === undefined ? {} : { advanced: advancedConfig }) };
279
+ }
280
+ function advanced(id, value) {
281
+ if (value === undefined)
282
+ return;
283
+ exact(value, ["rawArgs"], "Advanced Agent config");
284
+ validateAgentAdvancedArguments(id, value.rawArgs ?? []);
285
+ }
286
+ function validatePaths(values, label) {
287
+ optionalTexts(values, label);
288
+ for (const value of values ?? [])
289
+ absolutePath(value, label);
290
+ }
291
+ function canonicalDirectories(values) {
292
+ return [...new Set(values.map((value) => {
293
+ absolutePath(value, "Additional directory");
294
+ let path;
295
+ try {
296
+ path = realpathSync(value);
297
+ }
298
+ catch {
299
+ throw new Error("Additional directory does not exist or cannot be resolved.");
300
+ }
301
+ if (!statSync(path).isDirectory())
302
+ throw new Error("Additional directory is not a directory.");
303
+ return path;
304
+ }))].sort();
305
+ }
306
+ function absolutePath(value, label) {
307
+ text(value, label);
308
+ if (!isAbsolute(value) || resolve(value) !== value || /[\r\n\0{}]/.test(value)) {
309
+ throw new Error(`${label} must be an absolute canonical path.`);
310
+ }
311
+ }
312
+ function optionalText(value, label) {
313
+ if (value !== undefined)
314
+ text(value, label);
315
+ }
316
+ function optionalTexts(values, label) {
317
+ if (values === undefined)
318
+ return;
319
+ if (!Array.isArray(values))
320
+ throw new Error(`${label} list must be an array.`);
321
+ values.forEach((value) => text(value, label));
322
+ }
323
+ function text(value, label) {
324
+ if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
325
+ throw new Error(`${label} must be a non-empty string.`);
326
+ }
327
+ }
328
+ function exact(value, keys, label) {
329
+ if (value === null || typeof value !== "object" || Array.isArray(value)
330
+ || Object.keys(value).some((key) => !keys.includes(key)))
331
+ throw new Error(`${label} contains an unsupported field.`);
332
+ }
333
+ function nativeId(value) {
334
+ text(value, "Native session id");
335
+ if (value.trim() !== value)
336
+ throw new Error("Native session id must not contain surrounding whitespace.");
337
+ return value;
338
+ }