@xingular/harness 0.1.6 → 2.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.
- package/CHANGELOG.md +25 -0
- package/NOTICE.md +38 -0
- package/README.md +139 -28
- package/dist/cli.js +9598 -2201
- package/dist/terminal-pi.js +808 -16
- package/docs/README.md +15 -0
- package/docs/decisions/ADR-0001-owned-terminal.md +87 -0
- package/docs/decisions/ADR-0002-native-subagent-lifecycle.md +49 -0
- package/docs/decisions/ADR-0003-shared-asynchronous-subagents.md +68 -0
- package/docs/decisions/ADR-0004-fullscreen-terminal-layout.md +49 -0
- package/docs/decisions/ADR-0005-app-provided-oauth-clients.md +54 -0
- package/docs/terminal-tui.md +344 -0
- package/package.json +11 -4
- package/src/accounts/accounts.test.ts +197 -1
- package/src/accounts/accounts.ts +266 -28
- package/src/cli.ts +7 -3
- package/src/core/bus.test.ts +22 -0
- package/src/core/bus.ts +17 -2
- package/src/harness.ts +27 -4
- package/src/hub/catalog.json +123 -0
- package/src/hub/cli.ts +59 -0
- package/src/hub/hub.test.ts +205 -0
- package/src/hub/hub.ts +256 -0
- package/src/hub/schema.ts +81 -0
- package/src/index.ts +12 -2
- package/src/mcp/connections.ts +39 -12
- package/src/mcp/service.test.ts +49 -3
- package/src/mcp/service.ts +14 -3
- package/src/mcp/types.ts +10 -0
- package/src/memory/api.ts +16 -2
- package/src/memory/blocks.ts +3 -1
- package/src/memory/caller.ts +13 -1
- package/src/memory/curation.test.ts +206 -0
- package/src/memory/curation.ts +64 -0
- package/src/memory/dispatch.ts +5 -0
- package/src/memory/maintenance.test.ts +17 -1
- package/src/memory/maintenance.ts +367 -18
- package/src/memory/operations.ts +12 -0
- package/src/memory/proposals.ts +16 -8
- package/src/memory/recall.test.ts +11 -0
- package/src/memory/recall.ts +11 -3
- package/src/memory/trajectory.test.ts +178 -0
- package/src/memory/trajectory.ts +220 -0
- package/src/memory/wiki.ts +2 -1
- package/src/memory/wikiIndex.ts +3 -2
- package/src/plugins/builtin.ts +4 -2
- package/src/plugins/catalogue.test.ts +1 -1
- package/src/server/methods.ts +43 -2
- package/src/server/server.test.ts +20 -1
- package/src/sessions/approvals.test.ts +49 -0
- package/src/sessions/approvals.ts +26 -4
- package/src/sessions/contracts.ts +29 -3
- package/src/sessions/conversation.test.ts +83 -0
- package/src/sessions/lineageJoin.ts +1 -1
- package/src/sessions/policy.ts +4 -0
- package/src/sessions/questionBatches.test.ts +110 -0
- package/src/sessions/registry.ts +145 -27
- package/src/sessions/runStore.ts +14 -0
- package/src/sessions/runner.ts +45 -4
- package/src/sessions/sessions.test.ts +137 -4
- package/src/sessions/sessions.ts +51 -15
- package/src/sessions/subagentService.test.ts +159 -0
- package/src/sessions/subagentService.ts +236 -0
- package/src/sessions/subagents.ts +97 -0
- package/src/skills/registry.ts +16 -10
- package/src/terminal/accountsMenu.test.ts +225 -0
- package/src/terminal/accountsMenu.ts +382 -0
- package/src/terminal/control.test.ts +23 -0
- package/src/terminal/control.ts +23 -0
- package/src/terminal/controller.ts +134 -7
- package/src/terminal/hubMenu.ts +24 -0
- package/src/terminal/launch.test.ts +5 -14
- package/src/terminal/launch.ts +2 -42
- package/src/terminal/piExtension.lifecycle.test.ts +55 -0
- package/src/terminal/piExtension.ts +91 -6
- package/src/terminal/socket.test.ts +23 -0
- package/src/terminal/socket.ts +55 -20
- package/src/terminal/standalone.test.ts +37 -0
- package/src/terminal/standalone.ts +28 -7
- package/src/terminal/terminal.test.ts +220 -16
- package/src/terminal/terminals.ts +234 -16
- package/src/terminal/trajectory.test.ts +66 -0
- package/src/terminal/trajectory.ts +44 -0
- package/src/terminal/tui/application.test.ts +290 -0
- package/src/terminal/tui/application.ts +653 -0
- package/src/terminal/tui/commands.test.ts +278 -0
- package/src/terminal/tui/commands.ts +443 -0
- package/src/terminal/tui/dialogs.ts +301 -0
- package/src/terminal/tui/editor.test.ts +113 -0
- package/src/terminal/tui/editor.ts +109 -0
- package/src/terminal/tui/keybindings.test.ts +30 -0
- package/src/terminal/tui/keybindings.ts +59 -0
- package/src/terminal/tui/piAdapter.ts +62 -0
- package/src/terminal/tui/planMode.test.ts +10 -0
- package/src/terminal/tui/planMode.ts +12 -0
- package/src/terminal/tui/presentation.test.ts +79 -0
- package/src/terminal/tui/questions.test.ts +41 -0
- package/src/terminal/tui/questions.ts +47 -0
- package/src/terminal/tui/runtime.socket.test.ts +178 -0
- package/src/terminal/tui/runtime.test.ts +225 -0
- package/src/terminal/tui/runtime.ts +403 -0
- package/src/terminal/tui/screen.test.ts +143 -0
- package/src/terminal/tui/screen.ts +121 -0
- package/src/terminal/tui/sessionUi.ts +24 -0
- package/src/terminal/tui/settings.test.ts +150 -0
- package/src/terminal/tui/settings.ts +188 -0
- package/src/terminal/tui/skillInvocation.test.ts +71 -0
- package/src/terminal/tui/skillInvocation.ts +51 -0
- package/src/terminal/tui/status.test.ts +48 -0
- package/src/terminal/tui/status.ts +82 -0
- package/src/terminal/tui/theme.ts +185 -0
- package/src/terminal/tui/tools.test.ts +145 -0
- package/src/terminal/tui/tools.ts +142 -0
- package/src/terminal/tui/trajectoryPanel.test.ts +99 -0
- package/src/terminal/tui/trajectoryPanel.ts +226 -0
- package/src/terminal/tui/transcript.test.ts +39 -0
- package/src/terminal/tui/transcript.ts +135 -0
- package/src/terminal/tui/ui.test.ts +313 -0
- package/src/terminal/tui/ui.ts +166 -0
- package/src/terminal/tui/welcome.ts +102 -0
- package/src/terminal/tui/workingStatus.test.ts +44 -0
- package/src/terminal/tui/workingStatus.ts +238 -0
- package/src/tools/askUser.test.ts +69 -0
- package/src/tools/askUser.ts +31 -17
- package/src/tools/definitions.ts +42 -1
- package/src/tools/maintenance.ts +29 -0
- package/src/tools/memoryTools.ts +1 -1
- package/src/tools/registryWrite.test.ts +101 -0
- package/src/tools/registryWrite.ts +182 -0
- package/src/tools/spawnSubagent.ts +47 -0
- package/src/tools/types.ts +2 -0
package/dist/terminal-pi.js
CHANGED
|
@@ -6,10 +6,11 @@ import { createConnection } from "node:net";
|
|
|
6
6
|
var SOCKET_ENV = "XINGULAR_HARNESS_SOCKET";
|
|
7
7
|
var TOKEN_ENV = "XINGULAR_HARNESS_TOKEN";
|
|
8
8
|
var SocketError = class extends Error {
|
|
9
|
-
constructor(code,
|
|
10
|
-
super(
|
|
9
|
+
constructor(code, message2) {
|
|
10
|
+
super(message2);
|
|
11
11
|
this.code = code;
|
|
12
12
|
}
|
|
13
|
+
code;
|
|
13
14
|
};
|
|
14
15
|
function connectionFromEnv(env = process.env) {
|
|
15
16
|
const socket = env[SOCKET_ENV];
|
|
@@ -60,6 +61,322 @@ function terminalCall(event, data = {}, timeoutMs = 6e5) {
|
|
|
60
61
|
return request(connectionFromEnv(), { event, data }, event, timeoutMs);
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
// src/terminal/accountsMenu.ts
|
|
65
|
+
var HEALTH = {
|
|
66
|
+
ok: "Ready",
|
|
67
|
+
limited: "Limit reached",
|
|
68
|
+
auth_expired: "Sign in again",
|
|
69
|
+
signed_out: "Signed out",
|
|
70
|
+
error: "Needs attention",
|
|
71
|
+
unknown: "Not checked"
|
|
72
|
+
};
|
|
73
|
+
var ADD = "Add account";
|
|
74
|
+
var POOLS = "Pools";
|
|
75
|
+
var CHECK_ALL = "Check all accounts";
|
|
76
|
+
var DONE = "Done";
|
|
77
|
+
var BACK = "Back";
|
|
78
|
+
var USE_HERE = "Use for this session (restarts it)";
|
|
79
|
+
var MAKE_DEFAULT = "Make default for new sessions";
|
|
80
|
+
var RENAME = "Rename";
|
|
81
|
+
var CHECK = "Check";
|
|
82
|
+
var DETAILS = "Details";
|
|
83
|
+
var ACTIVITY = "Activity";
|
|
84
|
+
var SIGN_IN = "Sign in";
|
|
85
|
+
var REMOVE_PROVIDER = "Remove a provider";
|
|
86
|
+
var SIGN_OUT = "Sign out";
|
|
87
|
+
var REMOVE = "Remove account";
|
|
88
|
+
var NEW_POOL = "New pool";
|
|
89
|
+
var EDIT = "Edit";
|
|
90
|
+
var DELETE = "Delete";
|
|
91
|
+
var SAVE_POOL = "Save pool";
|
|
92
|
+
var CANCEL = "Cancel";
|
|
93
|
+
var START_OVER = "Start over";
|
|
94
|
+
var REMOVE_LAST = "Remove the last one";
|
|
95
|
+
var CANCEL_SIGN_IN = "Cancel sign-in";
|
|
96
|
+
var CHECK_STATUS = "Check status";
|
|
97
|
+
var LOGIN_TIMEOUT_MS = 10 * 6e4;
|
|
98
|
+
var POLL_MS = 1e3;
|
|
99
|
+
var message = (error) => error instanceof Error ? error.message : String(error);
|
|
100
|
+
async function attempt(ui, action) {
|
|
101
|
+
try {
|
|
102
|
+
await action();
|
|
103
|
+
return true;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
ui.notify(message(error), "error");
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function resetIn(resetsAt, now2 = Date.now()) {
|
|
110
|
+
const minutes = Math.round((Date.parse(resetsAt) - now2) / 6e4);
|
|
111
|
+
if (Number.isNaN(minutes)) return resetsAt;
|
|
112
|
+
if (minutes <= 0) return "now";
|
|
113
|
+
if (minutes < 60) return `in ${minutes}m`;
|
|
114
|
+
if (minutes < 60 * 24) return `in ${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
115
|
+
return `in ${Math.floor(minutes / (60 * 24))}d`;
|
|
116
|
+
}
|
|
117
|
+
function accountRows(overview, session) {
|
|
118
|
+
return overview.accounts.map((account, index) => {
|
|
119
|
+
const mark2 = account.isDefault ? "\u25CF" : "\u25CB";
|
|
120
|
+
const here2 = session?.accountId === account.id ? " \xB7 this session" : "";
|
|
121
|
+
return { account, label: `${index + 1}. ${mark2} ${account.label} \xB7 ${HEALTH[account.state.health] ?? HEALTH.unknown}${here2}` };
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
var limitLine = (limit) => `${limit.label}: ${limit.usedPercent ?? "?"}% used${limit.resetsAt ? `, resets ${resetIn(limit.resetsAt)}` : ""}`;
|
|
125
|
+
function accountDetails(account) {
|
|
126
|
+
const state = account.state;
|
|
127
|
+
const lines2 = [
|
|
128
|
+
account.label,
|
|
129
|
+
`Health: ${HEALTH[state.health] ?? HEALTH.unknown}`,
|
|
130
|
+
`Default for new sessions: ${account.isDefault ? "yes" : "no"}`,
|
|
131
|
+
`Signed in: ${state.signedIn === null ? "not checked" : state.signedIn ? "yes" : "no"}`,
|
|
132
|
+
`Providers: ${state.piProviders.length ? state.piProviders.join(", ") : "none"}`,
|
|
133
|
+
state.plan ? `Plan: ${state.plan}` : null,
|
|
134
|
+
state.identity ? `Identity: ${state.identity}` : null,
|
|
135
|
+
`Last checked: ${state.lastCheckedAt ? new Date(state.lastCheckedAt).toLocaleString() : "never"}`,
|
|
136
|
+
state.lastError ? `Last error: ${state.lastError.message}${state.lastError.resetsAt ? ` (resets ${resetIn(state.lastError.resetsAt)})` : ""}` : null,
|
|
137
|
+
...state.limits.map(limitLine),
|
|
138
|
+
state.limits.length ? null : "Usage limits: Pi providers do not report them.",
|
|
139
|
+
state.capabilities.length ? `Detected: ${state.capabilities.join(", ")}` : null
|
|
140
|
+
];
|
|
141
|
+
return lines2.filter((line) => line !== null).join("\n");
|
|
142
|
+
}
|
|
143
|
+
function loginStep(step) {
|
|
144
|
+
if (step.type === "auth_url" && typeof step.url === "string") return `Open this link to sign in:
|
|
145
|
+
${step.url}${typeof step.instructions === "string" ? `
|
|
146
|
+
${step.instructions}` : ""}`;
|
|
147
|
+
if (step.type === "device_code" && typeof step.userCode === "string") return `Enter the code ${step.userCode} at ${String(step.verificationUri ?? "")}`;
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
async function runAccountsMenu(ui, call, session) {
|
|
151
|
+
for (; ; ) {
|
|
152
|
+
const overview = await call("accounts.list");
|
|
153
|
+
const rows = accountRows(overview, session);
|
|
154
|
+
const options = [...rows.map((row2) => row2.label), ADD, POOLS, CHECK_ALL, DONE];
|
|
155
|
+
const picked = await ui.select("Accounts", options);
|
|
156
|
+
if (!picked || picked === DONE) return;
|
|
157
|
+
if (picked === ADD) {
|
|
158
|
+
await addAccount(ui, call);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (picked === POOLS) {
|
|
162
|
+
await poolsMenu(ui, call);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (picked === CHECK_ALL) {
|
|
166
|
+
await attempt(ui, () => call("accounts.refresh"));
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const row = rows[options.indexOf(picked)];
|
|
170
|
+
if (!row) continue;
|
|
171
|
+
if (await accountMenu(ui, call, session, row.account.id) === "relaunch") return "relaunch";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function addAccount(ui, call) {
|
|
175
|
+
const label = await ui.input("Name for the new account", "Work, Personal, \u2026");
|
|
176
|
+
if (!label?.trim()) return;
|
|
177
|
+
if (await attempt(ui, () => call("accounts.create", { label: label.trim(), authKind: "subscription" }))) ui.notify("Account added. Sign in to use it.", "info");
|
|
178
|
+
}
|
|
179
|
+
async function accountMenu(ui, call, session, accountId) {
|
|
180
|
+
const isSession = session.accountId === accountId;
|
|
181
|
+
for (; ; ) {
|
|
182
|
+
const overview = await call("accounts.list");
|
|
183
|
+
const account = overview.accounts.find((candidate) => candidate.id === accountId);
|
|
184
|
+
if (!account) return;
|
|
185
|
+
const actions = [
|
|
186
|
+
...isSession ? [] : [USE_HERE],
|
|
187
|
+
...account.isDefault ? [] : [MAKE_DEFAULT],
|
|
188
|
+
RENAME,
|
|
189
|
+
CHECK,
|
|
190
|
+
DETAILS,
|
|
191
|
+
ACTIVITY,
|
|
192
|
+
SIGN_IN,
|
|
193
|
+
...account.state.piProviders.length ? [REMOVE_PROVIDER] : [],
|
|
194
|
+
SIGN_OUT,
|
|
195
|
+
// The session's own account is in use, so it cannot be removed from inside the session.
|
|
196
|
+
...isSession ? [] : [REMOVE],
|
|
197
|
+
BACK
|
|
198
|
+
];
|
|
199
|
+
const action = await ui.select(`${account.label} \xB7 ${HEALTH[account.state.health] ?? HEALTH.unknown}`, actions);
|
|
200
|
+
if (!action || action === BACK) return;
|
|
201
|
+
if (action === USE_HERE) {
|
|
202
|
+
if (!await ui.confirm("Switch this session?", `This ends the terminal's Pi process and resumes the conversation on ${account.label}. Work in progress stops.`)) continue;
|
|
203
|
+
if (await attempt(ui, () => call("accounts.relaunch", { accountId }))) {
|
|
204
|
+
ui.notify(`Switching to ${account.label}.`, "info");
|
|
205
|
+
return "relaunch";
|
|
206
|
+
}
|
|
207
|
+
} else if (action === MAKE_DEFAULT) {
|
|
208
|
+
if (await attempt(ui, () => call("accounts.setDefault", { accountId }))) ui.notify(`${account.label} is the default for new sessions.`, "info");
|
|
209
|
+
} else if (action === RENAME) {
|
|
210
|
+
const label = await ui.input("New name", account.label);
|
|
211
|
+
if (label?.trim()) await attempt(ui, () => call("accounts.rename", { accountId, label: label.trim() }));
|
|
212
|
+
} else if (action === CHECK) {
|
|
213
|
+
if (await attempt(ui, () => call("accounts.refresh", { accountId }))) ui.notify("Checked.", "info");
|
|
214
|
+
} else if (action === DETAILS) {
|
|
215
|
+
await ui.select(accountDetails(account), [BACK]);
|
|
216
|
+
} else if (action === ACTIVITY) {
|
|
217
|
+
await showActivity(ui, call, accountId, account.label);
|
|
218
|
+
} else if (action === SIGN_IN) {
|
|
219
|
+
await signIn(ui, call, account);
|
|
220
|
+
} else if (action === REMOVE_PROVIDER) {
|
|
221
|
+
const provider = await ui.select("Remove which provider?", [...account.state.piProviders, BACK]);
|
|
222
|
+
if (!provider || provider === BACK) continue;
|
|
223
|
+
if (await ui.confirm(`Remove ${provider}?`, `The stored ${provider} sign-in for ${account.label} is deleted from this device.`)) await attempt(ui, () => call("accounts.removeProvider", { accountId, provider }));
|
|
224
|
+
} else if (action === SIGN_OUT) {
|
|
225
|
+
if (await ui.confirm(`Sign out of ${account.label}?`, "Stored sign-ins are deleted. The account stays, so you can sign in again.")) await attempt(ui, () => call("accounts.signOut", { accountId }));
|
|
226
|
+
} else if (action === REMOVE) {
|
|
227
|
+
const last = overview.accounts.length === 1;
|
|
228
|
+
const consequence = last ? "This is the only account. New sessions will ask you to add one." : account.isDefault ? "New sessions will use another account." : "Sessions pinned to it will ask for another account next time they start.";
|
|
229
|
+
if (!await ui.confirm(`Remove ${account.label}?`, `Its stored sign-in and its runtime folder are deleted. ${consequence}`)) continue;
|
|
230
|
+
if (await attempt(ui, () => call("accounts.remove", { accountId }))) ui.notify(`${account.label} was removed.`, "info");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function showActivity(ui, call, accountId, label) {
|
|
236
|
+
const entries = await call("accounts.audit", { accountId, limit: 30 });
|
|
237
|
+
const lines2 = entries.map((entry) => `${new Date(entry.at).toLocaleString()} \xB7 ${entry.action} \xB7 ${entry.detail}`);
|
|
238
|
+
await ui.select(`${label}: activity
|
|
239
|
+
${lines2.join("\n") || "Nothing has happened here yet."}`, [BACK]);
|
|
240
|
+
}
|
|
241
|
+
async function signIn(ui, call, account) {
|
|
242
|
+
const providers = await call("accounts.providers");
|
|
243
|
+
if (!providers.length) {
|
|
244
|
+
ui.notify("No provider here can be signed in to.", "warning");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
const labels = providers.map((provider2, index2) => `${index2 + 1}. ${provider2.name}${account.state.piProviders.includes(provider2.id) ? " \xB7 signed in" : ""}`);
|
|
248
|
+
const pickedLabel = await ui.select(`Sign in ${account.label} to which provider?`, [...labels, BACK]);
|
|
249
|
+
const index = labels.indexOf(pickedLabel ?? "");
|
|
250
|
+
const provider = providers[index];
|
|
251
|
+
if (!provider) return;
|
|
252
|
+
const methods = [...provider.oauth ? [provider.oauthLabel ?? "Sign in with the browser"] : [], ...provider.apiKey ? ["Enter an API key"] : []];
|
|
253
|
+
if (!methods.length) {
|
|
254
|
+
ui.notify(`${provider.name} has no sign-in from a terminal.`, "warning");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const method = methods.length === 1 ? methods[0] : await ui.select(`${provider.name}: how to sign in`, [...methods, BACK]);
|
|
258
|
+
if (!method || method === BACK) return;
|
|
259
|
+
if (method === "Enter an API key") {
|
|
260
|
+
const key = await ui.input(`API key for ${provider.name}`, "Paste the key");
|
|
261
|
+
if (!key?.trim()) return;
|
|
262
|
+
if (await attempt(ui, () => call("accounts.setApiKey", { accountId: account.id, provider: provider.id, key: key.trim() }))) ui.notify(`Key saved for ${provider.name}.`, "info");
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
await runLogin(ui, call, account.id, provider.id);
|
|
266
|
+
}
|
|
267
|
+
async function runLogin(ui, call, accountId, provider) {
|
|
268
|
+
let loginId;
|
|
269
|
+
try {
|
|
270
|
+
loginId = (await call("accounts.loginStart", { accountId, provider })).loginId;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
ui.notify(message(error), "error");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
return followLogin(ui, call, accountId, loginId);
|
|
276
|
+
}
|
|
277
|
+
async function followLogin(ui, call, accountId, loginId) {
|
|
278
|
+
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
279
|
+
let shownRevision = -1;
|
|
280
|
+
for (; ; ) {
|
|
281
|
+
const snapshot = await call("accounts.loginStatus", { accountId });
|
|
282
|
+
if (!snapshot || snapshot.loginId !== loginId) return;
|
|
283
|
+
if (snapshot.status !== "running") return reportLogin(ui, snapshot);
|
|
284
|
+
if (snapshot.prompt) {
|
|
285
|
+
await answerPrompt(ui, call, loginId, snapshot.prompt);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const step = [...snapshot.events].reverse().map(loginStep).find((text3) => text3 !== null);
|
|
289
|
+
if (step && snapshot.revision !== shownRevision) {
|
|
290
|
+
shownRevision = snapshot.revision;
|
|
291
|
+
const choice = await ui.select(`${step}
|
|
292
|
+
|
|
293
|
+
Finish in your browser, then choose Check status.`, [CHECK_STATUS, CANCEL_SIGN_IN]);
|
|
294
|
+
if (choice === CANCEL_SIGN_IN) {
|
|
295
|
+
await call("accounts.loginCancel", { accountId, loginId });
|
|
296
|
+
ui.notify("Sign-in cancelled.", "warning");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (Date.now() > deadline) {
|
|
302
|
+
await call("accounts.loginCancel", { accountId, loginId });
|
|
303
|
+
ui.notify("Sign-in timed out and was cancelled.", "error");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function reportLogin(ui, snapshot) {
|
|
310
|
+
if (snapshot.status === "completed") ui.notify("Signed in.", "info");
|
|
311
|
+
else if (snapshot.status === "cancelled") ui.notify("Sign-in was cancelled.", "warning");
|
|
312
|
+
else ui.notify(snapshot.error ?? "Sign-in failed.", "error");
|
|
313
|
+
}
|
|
314
|
+
async function answerPrompt(ui, call, loginId, prompt) {
|
|
315
|
+
const promptId = String(prompt.promptId ?? "");
|
|
316
|
+
const detail = prompt.prompt ?? {};
|
|
317
|
+
const text3 = detail.message ?? "Sign-in needs an answer.";
|
|
318
|
+
let value;
|
|
319
|
+
if (detail.type === "select" && Array.isArray(detail.options)) {
|
|
320
|
+
if (detail.options.every((option) => typeof option === "string")) {
|
|
321
|
+
value = await ui.select(text3, detail.options);
|
|
322
|
+
} else if (detail.options.every((option) => typeof option === "object" && option !== null && "id" in option && typeof option.id === "string" && "label" in option && typeof option.label === "string")) {
|
|
323
|
+
const options = detail.options;
|
|
324
|
+
const selected = await ui.select(text3, options.map((option) => option.label));
|
|
325
|
+
value = options.find((option) => option.label === selected)?.id;
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
value = await ui.input(text3, detail.placeholder ?? void 0);
|
|
329
|
+
}
|
|
330
|
+
await call("accounts.loginRespond", { loginId, promptId, value: value ?? null });
|
|
331
|
+
}
|
|
332
|
+
async function poolsMenu(ui, call) {
|
|
333
|
+
for (; ; ) {
|
|
334
|
+
const overview = await call("accounts.list");
|
|
335
|
+
const names = new Map(overview.accounts.map((account) => [account.id, account.label]));
|
|
336
|
+
const labels = overview.pools.map((pool2, index) => `${index + 1}. ${pool2.label} \xB7 ${pool2.accountIds.map((id) => names.get(id) ?? "removed account").join(" \u2192 ")}`);
|
|
337
|
+
const options = [NEW_POOL, ...labels, BACK];
|
|
338
|
+
const picked = await ui.select("Pools: new sessions use the first ready account in a pool, in this order", options);
|
|
339
|
+
if (!picked || picked === BACK) return;
|
|
340
|
+
if (picked === NEW_POOL) {
|
|
341
|
+
await editPool(ui, call, overview, null);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const pool = overview.pools[options.indexOf(picked) - 1];
|
|
345
|
+
if (!pool) continue;
|
|
346
|
+
const action = await ui.select(pool.label, [EDIT, DELETE, BACK]);
|
|
347
|
+
if (action === EDIT) await editPool(ui, call, overview, pool);
|
|
348
|
+
else if (action === DELETE && await ui.confirm(`Delete ${pool.label}?`, "The accounts stay. Only the pool is removed.")) await attempt(ui, () => call("accounts.removePool", { poolId: pool.id }));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function editPool(ui, call, overview, pool) {
|
|
352
|
+
const label = await ui.input("Pool name", pool?.label);
|
|
353
|
+
if (!label?.trim()) return;
|
|
354
|
+
const names = new Map(overview.accounts.map((account) => [account.id, account.label]));
|
|
355
|
+
const order = [...pool?.accountIds ?? []];
|
|
356
|
+
for (; ; ) {
|
|
357
|
+
const ordered = order.map((id, index) => `${index + 1}. ${names.get(id) ?? "removed account"}`).join(" ");
|
|
358
|
+
const free = overview.accounts.filter((account) => !order.includes(account.id));
|
|
359
|
+
const addOptions = free.map((account) => `Add ${account.label}`);
|
|
360
|
+
const options = [...addOptions, ...order.length ? [REMOVE_LAST, START_OVER] : [], SAVE_POOL, CANCEL];
|
|
361
|
+
const picked = await ui.select(`${label.trim()}: ${ordered || "no accounts yet"}`, options);
|
|
362
|
+
if (!picked || picked === CANCEL) return;
|
|
363
|
+
if (picked === SAVE_POOL) {
|
|
364
|
+
if (!order.length) {
|
|
365
|
+
ui.notify("A pool needs at least one account.", "error");
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (await attempt(ui, () => call("accounts.savePool", { pool: { id: pool?.id, label: label.trim(), accountIds: order } }))) ui.notify("Pool saved.", "info");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (picked === START_OVER) order.length = 0;
|
|
372
|
+
else if (picked === REMOVE_LAST) order.pop();
|
|
373
|
+
else {
|
|
374
|
+
const account = free[addOptions.indexOf(picked)];
|
|
375
|
+
if (account) order.push(account.id);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
63
380
|
// src/terminal/pluginMenu.ts
|
|
64
381
|
var here = (attachment, projectId) => {
|
|
65
382
|
if (projectId && attachment.on.projectIds.includes(projectId)) return true;
|
|
@@ -90,8 +407,8 @@ var SWITCH_OFF_EVERYWHERE = "Switch off everywhere";
|
|
|
90
407
|
var SWITCH_ON_EVERYWHERE = "Switch on everywhere";
|
|
91
408
|
var TRUST = "Trust its code";
|
|
92
409
|
var UNTRUST = "Stop trusting its code";
|
|
93
|
-
var
|
|
94
|
-
var
|
|
410
|
+
var BACK2 = "Back";
|
|
411
|
+
var DONE2 = "Done";
|
|
95
412
|
function rowActions(row, projectId) {
|
|
96
413
|
const switchable = row.whole ? row.plugin.attachment !== null : true;
|
|
97
414
|
const actions = [];
|
|
@@ -100,7 +417,7 @@ function rowActions(row, projectId) {
|
|
|
100
417
|
actions.push(row.on ? SWITCH_OFF_EVERYWHERE : SWITCH_ON_EVERYWHERE);
|
|
101
418
|
}
|
|
102
419
|
if (row.whole && row.plugin.code) actions.push(row.plugin.trust === "untrusted" ? TRUST : UNTRUST);
|
|
103
|
-
return [...actions,
|
|
420
|
+
return [...actions, BACK2];
|
|
104
421
|
}
|
|
105
422
|
async function runPluginsMenu(ui, call, projectId) {
|
|
106
423
|
let changed = false;
|
|
@@ -110,11 +427,11 @@ async function runPluginsMenu(ui, call, projectId) {
|
|
|
110
427
|
ui.notify("There is nothing to switch or trust here yet.", "info");
|
|
111
428
|
return;
|
|
112
429
|
}
|
|
113
|
-
const picked = await ui.select(`Plugins${changed ? " (changes apply when this session restarts)" : ""}`, [...rows.map((row2) => row2.label),
|
|
430
|
+
const picked = await ui.select(`Plugins${changed ? " (changes apply when this session restarts)" : ""}`, [...rows.map((row2) => row2.label), DONE2]);
|
|
114
431
|
const row = rows.find((candidate) => candidate.label === picked);
|
|
115
432
|
if (!row) break;
|
|
116
433
|
const action = await ui.select(`${row.plugin.title}: ${row.plugin.description || "No description."}`, rowActions(row, projectId));
|
|
117
|
-
if (!action || action ===
|
|
434
|
+
if (!action || action === BACK2) continue;
|
|
118
435
|
try {
|
|
119
436
|
if (action === TRUST || action === UNTRUST) {
|
|
120
437
|
const code = row.plugin.code;
|
|
@@ -137,15 +454,414 @@ Fingerprint ${code.hash.slice(0, 12)}. It runs inside the session and can do any
|
|
|
137
454
|
if (changed) ui.notify("Saved. It applies when this session restarts.", "info");
|
|
138
455
|
}
|
|
139
456
|
|
|
457
|
+
// src/terminal/hubMenu.ts
|
|
458
|
+
async function runHubMenu(ui, call) {
|
|
459
|
+
for (; ; ) {
|
|
460
|
+
const catalog = await call("hub.list");
|
|
461
|
+
const labels = catalog.entries.map((entry2) => `${entry2.title} \xB7 ${entry2.category} \xB7 ${entry2.id}@${entry2.version}`);
|
|
462
|
+
const picked = await ui.select("Hub", [...labels, "Refresh catalog", "Done"]);
|
|
463
|
+
if (picked === "Refresh catalog") {
|
|
464
|
+
const refreshed = await call("hub.list", { refresh: true });
|
|
465
|
+
if (refreshed.error) ui.notify(`${refreshed.error} Using ${refreshed.origin} content.`, "warning");
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const entry = catalog.entries[labels.indexOf(picked ?? "")];
|
|
469
|
+
if (!entry) return;
|
|
470
|
+
const summary = `${entry.description}
|
|
471
|
+
|
|
472
|
+
Publisher: ${entry.publisher}
|
|
473
|
+
License: ${entry.license}
|
|
474
|
+
Source: ${entry.sourceUrl}
|
|
475
|
+
|
|
476
|
+
${entry.delivery.kind === "document" ? "Saved as data for a compatible application. Nothing is created or enabled." : "Installed switched off. Code remains untrusted and approvals remain unchanged."}`;
|
|
477
|
+
if (!await ui.confirm(`Install ${entry.title} ${entry.version}?`, summary)) continue;
|
|
478
|
+
try {
|
|
479
|
+
const receipt = await call("hub.install", { id: entry.id, version: entry.version });
|
|
480
|
+
ui.notify(receipt.document ? "Saved in this library for a compatible application." : "Installed, switched off. Use /plugins to review and attach it. Changes apply after restarting the session.", "info");
|
|
481
|
+
} catch (error) {
|
|
482
|
+
ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// src/terminal/control.ts
|
|
488
|
+
var claims = /* @__PURE__ */ new Map();
|
|
489
|
+
function claimTerminalControl(connection = connectionFromEnv()) {
|
|
490
|
+
const identity = JSON.stringify([connection.socket, connection.token]);
|
|
491
|
+
let claim = claims.get(identity);
|
|
492
|
+
if (!claim) {
|
|
493
|
+
claim = socketCall(connection, "control.claim", {}, 15e3).then(({ key }) => {
|
|
494
|
+
if (!key) throw new Error("The terminal controller returned no control key.");
|
|
495
|
+
return key;
|
|
496
|
+
});
|
|
497
|
+
claims.set(identity, claim);
|
|
498
|
+
}
|
|
499
|
+
return claim;
|
|
500
|
+
}
|
|
501
|
+
async function controlCall(call, params = {}, timeoutMs = 12e4) {
|
|
502
|
+
const connection = connectionFromEnv();
|
|
503
|
+
const control = await claimTerminalControl(connection);
|
|
504
|
+
return socketCall(connection, call, params, timeoutMs, control);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/sessions/contracts.ts
|
|
508
|
+
import { z } from "zod";
|
|
509
|
+
var runStatusSchema = z.enum(["queued", "running", "waiting_for_approval", "waiting_for_input", "interrupted", "completed", "failed"]);
|
|
510
|
+
var runEventTypeSchema = z.enum([
|
|
511
|
+
"started",
|
|
512
|
+
"context.snapshot",
|
|
513
|
+
"message.delta",
|
|
514
|
+
"message.completed",
|
|
515
|
+
"message.steered",
|
|
516
|
+
"tool.started",
|
|
517
|
+
"tool.completed",
|
|
518
|
+
"retry.started",
|
|
519
|
+
"retry.ended",
|
|
520
|
+
"compaction.started",
|
|
521
|
+
"compaction.ended",
|
|
522
|
+
"approval.requested",
|
|
523
|
+
"approval.resolved",
|
|
524
|
+
"question.requested",
|
|
525
|
+
"question.answered",
|
|
526
|
+
"extension.notice",
|
|
527
|
+
"reaction.added",
|
|
528
|
+
"agent.message",
|
|
529
|
+
"artifact.added",
|
|
530
|
+
"file.changed",
|
|
531
|
+
"usage.updated",
|
|
532
|
+
"account.status",
|
|
533
|
+
"status.changed",
|
|
534
|
+
"failed",
|
|
535
|
+
"completed"
|
|
536
|
+
]);
|
|
537
|
+
var messageDeltaPayload = z.object({ messageId: z.string(), text: z.string() });
|
|
538
|
+
var messageCompletedPayload = z.object({ messageId: z.string(), role: z.enum(["user", "assistant"]), text: z.string() });
|
|
539
|
+
var messageSteeredPayload = z.object({ text: z.string() });
|
|
540
|
+
var contextSnapshotPayload = z.object({
|
|
541
|
+
systemPrompt: z.string().max(1e5),
|
|
542
|
+
contextUpdate: z.string().max(1e5).optional(),
|
|
543
|
+
tools: z.array(z.object({ name: z.string(), description: z.string().max(2e3), parameters: z.unknown() })).max(120),
|
|
544
|
+
systemPromptTruncated: z.boolean().optional(),
|
|
545
|
+
contextUpdateTruncated: z.boolean().optional(),
|
|
546
|
+
toolsTruncated: z.boolean().optional()
|
|
547
|
+
});
|
|
548
|
+
var toolAttachmentRefSchema = z.object({
|
|
549
|
+
id: z.string().min(1).max(300),
|
|
550
|
+
name: z.string().min(1).max(500),
|
|
551
|
+
mimeType: z.string().min(1).max(100)
|
|
552
|
+
}).strict();
|
|
553
|
+
var toolAttachmentRefsSchema = z.array(toolAttachmentRefSchema).max(16);
|
|
554
|
+
var toolPayload = z.object({
|
|
555
|
+
toolCallId: z.string(),
|
|
556
|
+
name: z.string(),
|
|
557
|
+
summary: z.string().max(200).optional(),
|
|
558
|
+
input: z.string().max(24e3).optional(),
|
|
559
|
+
inputTruncated: z.boolean().optional(),
|
|
560
|
+
output: z.string().max(24e3).optional(),
|
|
561
|
+
outputTruncated: z.boolean().optional(),
|
|
562
|
+
ok: z.boolean().optional(),
|
|
563
|
+
/** On `tool.completed`: how long the call ran, and the size of what it returned to the model. */
|
|
564
|
+
durationMs: z.number().optional(),
|
|
565
|
+
resultChars: z.number().optional(),
|
|
566
|
+
/** Opaque attachment references. Content stays outside the run event stream. */
|
|
567
|
+
attachments: toolAttachmentRefsSchema.optional()
|
|
568
|
+
});
|
|
569
|
+
var retryStartedPayload = z.object({ attempt: z.number(), maxAttempts: z.number(), delayMs: z.number(), error: z.string().max(1e3).optional() });
|
|
570
|
+
var retryEndedPayload = z.object({ attempt: z.number(), success: z.boolean(), error: z.string().max(1e3).optional() });
|
|
571
|
+
var compactionStartedPayload = z.object({ reason: z.string() });
|
|
572
|
+
var compactionEndedPayload = z.object({
|
|
573
|
+
reason: z.string(),
|
|
574
|
+
aborted: z.boolean(),
|
|
575
|
+
willRetry: z.boolean().optional(),
|
|
576
|
+
error: z.string().max(1e3).optional(),
|
|
577
|
+
tokensBefore: z.number().optional(),
|
|
578
|
+
tokensAfter: z.number().optional(),
|
|
579
|
+
summary: z.string().max(24e3).optional(),
|
|
580
|
+
summaryTruncated: z.boolean().optional()
|
|
581
|
+
});
|
|
582
|
+
var approvalAutomaticSchema = z.enum(["always", "remembered", "scope"]);
|
|
583
|
+
var approvalRequestedPayload = z.object({
|
|
584
|
+
approvalId: z.string(),
|
|
585
|
+
target: z.string(),
|
|
586
|
+
action: z.string(),
|
|
587
|
+
screenshotRef: z.string().optional(),
|
|
588
|
+
operation: z.object({ tool: z.string(), args: z.unknown() }).optional(),
|
|
589
|
+
/** A tool the user may choose to always allow for this session. */
|
|
590
|
+
rememberable: z.boolean().optional(),
|
|
591
|
+
/** Set when the request was granted as it was made, so nobody is waiting on it. */
|
|
592
|
+
automatic: approvalAutomaticSchema.optional()
|
|
593
|
+
});
|
|
594
|
+
var approvalResolvedPayload = z.object({ approvalId: z.string(), decision: z.enum(["approved", "denied", "native"]), automatic: approvalAutomaticSchema.optional() });
|
|
595
|
+
var questionOptionSchema = z.object({ label: z.string().min(1), description: z.string().optional() });
|
|
596
|
+
var questionBatchFields = {
|
|
597
|
+
batchId: z.string().min(1).optional(),
|
|
598
|
+
batchIndex: z.number().int().min(1).max(5).optional(),
|
|
599
|
+
batchCount: z.number().int().min(1).max(5).optional()
|
|
600
|
+
};
|
|
601
|
+
var questionBatchRefinement = (value, context) => {
|
|
602
|
+
const fields = [value.batchId, value.batchIndex, value.batchCount];
|
|
603
|
+
if (fields.some((field) => field !== void 0) && fields.some((field) => field === void 0)) {
|
|
604
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "Question batch fields must be supplied together." });
|
|
605
|
+
}
|
|
606
|
+
if (value.batchIndex !== void 0 && value.batchCount !== void 0 && value.batchIndex > value.batchCount) {
|
|
607
|
+
context.addIssue({ code: z.ZodIssueCode.custom, message: "Question batch index cannot exceed its count." });
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
var questionRequestedPayload = z.object({
|
|
611
|
+
questionId: z.string(),
|
|
612
|
+
question: z.string().min(1),
|
|
613
|
+
summary: z.string().min(1).max(100).optional(),
|
|
614
|
+
options: z.array(questionOptionSchema),
|
|
615
|
+
multiSelect: z.boolean(),
|
|
616
|
+
allowText: z.boolean(),
|
|
617
|
+
userOnly: z.boolean().optional(),
|
|
618
|
+
...questionBatchFields
|
|
619
|
+
}).superRefine(questionBatchRefinement);
|
|
620
|
+
var questionAnsweredPayload = z.object({
|
|
621
|
+
questionId: z.string(),
|
|
622
|
+
selected: z.array(z.string()),
|
|
623
|
+
text: z.string().optional(),
|
|
624
|
+
notes: z.record(z.string(), z.string().max(1e4)).optional(),
|
|
625
|
+
...questionBatchFields
|
|
626
|
+
}).superRefine(questionBatchRefinement);
|
|
627
|
+
var reactionAddedPayload = z.object({ emoji: z.string().min(1).refine((text3) => Buffer.byteLength(text3) <= 16 && !/[A-Za-z0-9\s]/.test(text3)) });
|
|
628
|
+
var agentMessagePayload = z.object({ direction: z.enum(["to", "from"]), agentId: z.string(), agentName: z.string(), text: z.string() });
|
|
629
|
+
var extensionNoticePayload = z.object({
|
|
630
|
+
kind: z.enum(["notify", "status", "widget", "title", "error"]),
|
|
631
|
+
level: z.enum(["info", "warning", "error"]).optional(),
|
|
632
|
+
key: z.string().optional(),
|
|
633
|
+
text: z.string(),
|
|
634
|
+
extension: z.string().optional()
|
|
635
|
+
});
|
|
636
|
+
var artifactPayload = z.object({ artifactId: z.string(), name: z.string(), file: z.unknown().optional() });
|
|
637
|
+
var fileChangedPayload = z.object({ path: z.string(), change: z.enum(["added", "modified", "deleted"]) });
|
|
638
|
+
var costSourceSchema = z.enum(["harness", "estimate", "mixed"]);
|
|
639
|
+
var usageRequestSchema = z.object({
|
|
640
|
+
/** Position in the run, from 1. */
|
|
641
|
+
index: z.number(),
|
|
642
|
+
responseId: z.string().optional(),
|
|
643
|
+
model: z.string().optional(),
|
|
644
|
+
provider: z.string().optional(),
|
|
645
|
+
inputTokens: z.number().optional(),
|
|
646
|
+
cacheReadTokens: z.number().optional(),
|
|
647
|
+
cacheWriteTokens: z.number().optional(),
|
|
648
|
+
outputTokens: z.number().optional(),
|
|
649
|
+
reasoningTokens: z.number().optional(),
|
|
650
|
+
costUsd: z.number().optional(),
|
|
651
|
+
costSource: z.enum(["harness", "estimate"]).optional(),
|
|
652
|
+
/** Separate provider-supplied summary; never generic private thinking content. */
|
|
653
|
+
reasoningSummary: z.string().max(4e3).optional(),
|
|
654
|
+
/** From sending the request to the end of the response, and to its first output. */
|
|
655
|
+
durationMs: z.number().optional(),
|
|
656
|
+
ttftMs: z.number().optional(),
|
|
657
|
+
stopReason: z.string().optional()
|
|
658
|
+
});
|
|
659
|
+
var usagePayloadSchema = z.object({
|
|
660
|
+
/** Input tokens that were not read from the provider's cache. */
|
|
661
|
+
inputTokens: z.number().optional(),
|
|
662
|
+
/** Includes reasoning tokens. */
|
|
663
|
+
outputTokens: z.number().optional(),
|
|
664
|
+
cacheReadTokens: z.number().optional(),
|
|
665
|
+
cacheWriteTokens: z.number().optional(),
|
|
666
|
+
reasoningTokens: z.number().optional(),
|
|
667
|
+
costUsd: z.number().optional(),
|
|
668
|
+
costSource: costSourceSchema.optional(),
|
|
669
|
+
/** Model requests made so far. */
|
|
670
|
+
requests: z.number().optional(),
|
|
671
|
+
/** Size of the latest request (not cumulative), and the model's limit. */
|
|
672
|
+
contextTokens: z.number().optional(),
|
|
673
|
+
contextWindow: z.number().optional(),
|
|
674
|
+
model: z.string().optional(),
|
|
675
|
+
provider: z.string().optional(),
|
|
676
|
+
/** The request this update reports. */
|
|
677
|
+
request: usageRequestSchema.optional()
|
|
678
|
+
});
|
|
679
|
+
var accountIssueCategorySchema = z.enum(["auth_expired", "rate_limited", "quota", "network", "unknown"]);
|
|
680
|
+
var accountLimitSchema = z.object({
|
|
681
|
+
id: z.string(),
|
|
682
|
+
label: z.string(),
|
|
683
|
+
usedPercent: z.number().nullable().optional(),
|
|
684
|
+
resetsAt: z.string().nullable().optional(),
|
|
685
|
+
windowMinutes: z.number().nullable().optional(),
|
|
686
|
+
status: z.string().nullable().optional()
|
|
687
|
+
});
|
|
688
|
+
var accountStatusPayload = z.object({
|
|
689
|
+
accountId: z.string(),
|
|
690
|
+
category: accountIssueCategorySchema.optional(),
|
|
691
|
+
message: z.string().optional(),
|
|
692
|
+
resetsAt: z.string().optional(),
|
|
693
|
+
plan: z.string().optional(),
|
|
694
|
+
limits: z.array(accountLimitSchema).optional()
|
|
695
|
+
});
|
|
696
|
+
var statusPayload = z.object({ status: runStatusSchema, reason: z.string().optional() });
|
|
697
|
+
var failedPayload = z.object({ code: z.string(), message: z.string() });
|
|
698
|
+
var emptyPayload = z.object({}).passthrough();
|
|
699
|
+
var runEventPayloadSchemas = {
|
|
700
|
+
started: emptyPayload,
|
|
701
|
+
"context.snapshot": contextSnapshotPayload,
|
|
702
|
+
"message.delta": messageDeltaPayload,
|
|
703
|
+
"message.completed": messageCompletedPayload,
|
|
704
|
+
"message.steered": messageSteeredPayload,
|
|
705
|
+
"tool.started": toolPayload,
|
|
706
|
+
"tool.completed": toolPayload,
|
|
707
|
+
"retry.started": retryStartedPayload,
|
|
708
|
+
"retry.ended": retryEndedPayload,
|
|
709
|
+
"compaction.started": compactionStartedPayload,
|
|
710
|
+
"compaction.ended": compactionEndedPayload,
|
|
711
|
+
"approval.requested": approvalRequestedPayload,
|
|
712
|
+
"approval.resolved": approvalResolvedPayload,
|
|
713
|
+
"question.requested": questionRequestedPayload,
|
|
714
|
+
"question.answered": questionAnsweredPayload,
|
|
715
|
+
"extension.notice": extensionNoticePayload,
|
|
716
|
+
"reaction.added": reactionAddedPayload,
|
|
717
|
+
"agent.message": agentMessagePayload,
|
|
718
|
+
"artifact.added": artifactPayload,
|
|
719
|
+
"file.changed": fileChangedPayload,
|
|
720
|
+
"usage.updated": usagePayloadSchema,
|
|
721
|
+
"account.status": accountStatusPayload,
|
|
722
|
+
"status.changed": statusPayload,
|
|
723
|
+
failed: failedPayload,
|
|
724
|
+
completed: emptyPayload
|
|
725
|
+
};
|
|
726
|
+
var runEventSchema = z.object({
|
|
727
|
+
id: z.string(),
|
|
728
|
+
runId: z.string(),
|
|
729
|
+
sessionId: z.string(),
|
|
730
|
+
sequence: z.number().int().nonnegative(),
|
|
731
|
+
timestamp: z.string(),
|
|
732
|
+
type: runEventTypeSchema,
|
|
733
|
+
payload: z.unknown(),
|
|
734
|
+
/** The native event this one was derived from, kept as evidence. */
|
|
735
|
+
native: z.unknown().optional(),
|
|
736
|
+
/** Set when consecutive text deltas were merged for relay; covers sequences coalescedFrom..sequence. */
|
|
737
|
+
coalescedFrom: z.number().int().nonnegative().optional()
|
|
738
|
+
}).superRefine((event, context) => {
|
|
739
|
+
if (!runEventPayloadSchemas[event.type].safeParse(event.payload).success) {
|
|
740
|
+
context.addIssue({ code: "custom", path: ["payload"], message: `Invalid payload for ${event.type}` });
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
var text = z.string().nullish().transform((value) => value ?? null);
|
|
744
|
+
var runRecordSchema = z.object({
|
|
745
|
+
resumedFromRunId: text,
|
|
746
|
+
parentRunId: text,
|
|
747
|
+
relation: text,
|
|
748
|
+
terminalReason: text,
|
|
749
|
+
id: z.string(),
|
|
750
|
+
projectId: text,
|
|
751
|
+
mode: z.string(),
|
|
752
|
+
harness: z.string(),
|
|
753
|
+
sessionType: z.string(),
|
|
754
|
+
sessionId: z.string(),
|
|
755
|
+
nativeSessionId: text,
|
|
756
|
+
workingDirectory: text,
|
|
757
|
+
worktreeId: text,
|
|
758
|
+
goal: text,
|
|
759
|
+
attachmentCount: z.number().int().nonnegative().default(0),
|
|
760
|
+
startedAt: z.string(),
|
|
761
|
+
endedAt: text,
|
|
762
|
+
status: z.string(),
|
|
763
|
+
exitCode: z.number().int().nullish().transform((value) => value ?? null),
|
|
764
|
+
handoffId: text,
|
|
765
|
+
accountId: text,
|
|
766
|
+
agentId: text,
|
|
767
|
+
trigger: text
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
// src/core/index.ts
|
|
771
|
+
import { DatabaseSync } from "node:sqlite";
|
|
772
|
+
|
|
773
|
+
// src/sessions/subagents.ts
|
|
774
|
+
var SUBAGENT_RECEIPT = "xingularSubagents";
|
|
775
|
+
var SUBAGENT_OWNER = "xingular-subagent-owner";
|
|
776
|
+
var SUBAGENT_RESULTS = "xingular-subagent-results";
|
|
777
|
+
function subagentHistory(entries) {
|
|
778
|
+
const owners = [];
|
|
779
|
+
const receipts = [];
|
|
780
|
+
for (const raw of entries) {
|
|
781
|
+
if (!raw || typeof raw !== "object") continue;
|
|
782
|
+
const entry = raw;
|
|
783
|
+
if (entry.type === "custom" && entry.customType === SUBAGENT_OWNER && typeof entry.data?.runId === "string") owners.push(entry.data.runId);
|
|
784
|
+
const message2 = entry.type === "message" ? entry.message : entry;
|
|
785
|
+
const receipt = message2?.details?.[SUBAGENT_RECEIPT];
|
|
786
|
+
if (!receipt || receipt.version !== 1 || typeof receipt.ownerRunId !== "string") continue;
|
|
787
|
+
const strings = (value) => Array.isArray(value) && value.every((id) => typeof id === "string");
|
|
788
|
+
if (receipt.spawned !== void 0 && !strings(receipt.spawned) || receipt.delivered !== void 0 && !strings(receipt.delivered)) continue;
|
|
789
|
+
receipts.push(receipt);
|
|
790
|
+
}
|
|
791
|
+
return { owners, receipts };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/tools/askUser.ts
|
|
795
|
+
var ASK_USER = "ask_user";
|
|
796
|
+
var MAX_OPTIONS = 6;
|
|
797
|
+
var text2 = (value) => typeof value === "string" ? value.trim() : "";
|
|
798
|
+
function describeAnswer(answer) {
|
|
799
|
+
const parts = [];
|
|
800
|
+
if (answer.selected.length) parts.push(`The user chose: ${answer.selected.join(", ")}.`);
|
|
801
|
+
if (answer.text) parts.push(answer.selected.length ? `They added: ${answer.text}` : `The user answered: ${answer.text}`);
|
|
802
|
+
for (const [choice, note] of Object.entries(answer.notes ?? {})) parts.push(`Their note for ${choice}: ${note}`);
|
|
803
|
+
return parts.join(" ") || "The user did not answer.";
|
|
804
|
+
}
|
|
805
|
+
function askUserTool(run2) {
|
|
806
|
+
return {
|
|
807
|
+
name: ASK_USER,
|
|
808
|
+
label: "Ask the user",
|
|
809
|
+
description: "Ask the user one to five related questions in one request and wait until they answer the batch. Give each question a concise 2\u20133 word summary and aim to keep each question to 25 words or fewer. Offer up to 6 short options when the choices are clear; every option must include a helpful description of 25 words or fewer. Every question includes a free-text \u201CSomething else\u201D answer. Set multiSelect when several can apply. Use it when you need a decision, not for small talk.",
|
|
810
|
+
parameters: (Type2) => Type2.Object({
|
|
811
|
+
questions: Type2.Array(Type2.Object({
|
|
812
|
+
summary: Type2.String({ description: "A concise 2\u20133 word summary label" }),
|
|
813
|
+
question: Type2.String({ description: "A clear, standalone question; aim for 25 words or fewer" }),
|
|
814
|
+
options: Type2.Optional(
|
|
815
|
+
Type2.Array(Type2.Object({
|
|
816
|
+
label: Type2.String({ description: "A short choice" }),
|
|
817
|
+
description: Type2.String({ description: "Required. Briefly explain this choice in 25 words or fewer." })
|
|
818
|
+
}), { maxItems: MAX_OPTIONS })
|
|
819
|
+
),
|
|
820
|
+
multiSelect: Type2.Optional(Type2.Boolean())
|
|
821
|
+
}), { minItems: 1, maxItems: 5 })
|
|
822
|
+
}),
|
|
823
|
+
execute: async (input) => {
|
|
824
|
+
if (!Array.isArray(input.questions) || input.questions.length < 1 || input.questions.length > 5) return "Ask between one and five questions.";
|
|
825
|
+
const questions = input.questions.map((raw) => {
|
|
826
|
+
const item = raw;
|
|
827
|
+
const summary = text2(item.summary);
|
|
828
|
+
const question = text2(item.question);
|
|
829
|
+
const options = (Array.isArray(item.options) ? item.options : []).map((option) => ({ label: text2(option.label), description: text2(option.description) })).filter((option) => option.label).slice(0, MAX_OPTIONS);
|
|
830
|
+
return { summary, question, options, multiSelect: options.length > 1 && item.multiSelect === true, allowText: true };
|
|
831
|
+
});
|
|
832
|
+
if (questions.some((question) => !question.summary || question.summary.length > 100)) return "Each question needs a short summary of no more than 100 characters.";
|
|
833
|
+
if (questions.some((question) => !question.question)) return "Every question needs a question sentence.";
|
|
834
|
+
if (questions.some((question) => question.options.some((option) => !option.description))) return "Every choice needs a short description. Aim for 25 words or fewer.";
|
|
835
|
+
const answers = await run2.requestAnswers(questions);
|
|
836
|
+
return questions.map((question, index) => `${question.summary}: ${describeAnswer(answers[index])}`).join("\n");
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
140
841
|
// src/terminal/piExtension.ts
|
|
141
842
|
function resultSize(result) {
|
|
142
843
|
const content = result?.content;
|
|
143
844
|
if (!Array.isArray(content)) return typeof result === "string" ? result.length : null;
|
|
144
845
|
return content.reduce((total, part) => total + (part?.type === "text" && typeof part.text === "string" ? part.text.length : 0), 0);
|
|
145
846
|
}
|
|
847
|
+
async function withParentCancellation(signal, work) {
|
|
848
|
+
const stop = () => {
|
|
849
|
+
void controlCall("terminal.interrupt").catch(() => {
|
|
850
|
+
});
|
|
851
|
+
};
|
|
852
|
+
if (signal?.aborted) stop();
|
|
853
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
854
|
+
try {
|
|
855
|
+
return await work();
|
|
856
|
+
} finally {
|
|
857
|
+
signal?.removeEventListener("abort", stop);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
146
860
|
async function piExtension_default(pi) {
|
|
147
861
|
await terminalCall("startup", { phase: "Pi extension loaded" });
|
|
148
862
|
let context = null;
|
|
863
|
+
let nativeSubagents = false;
|
|
864
|
+
let subagentParent = false;
|
|
149
865
|
let timer = null;
|
|
150
866
|
let polling = false;
|
|
151
867
|
let failure;
|
|
@@ -160,19 +876,48 @@ async function piExtension_default(pi) {
|
|
|
160
876
|
try {
|
|
161
877
|
await terminalCall("startup", { phase: stage });
|
|
162
878
|
const connection = connectionFromEnv();
|
|
163
|
-
const control = await
|
|
879
|
+
const control = await claimTerminalControl(connection).catch(() => null);
|
|
880
|
+
pi.registerCommand("hub", {
|
|
881
|
+
description: "Browse and install reviewed skills, plugins, and reusable documents",
|
|
882
|
+
handler: async (_args, ctx) => {
|
|
883
|
+
if (!control) return ctx.ui.notify("This session cannot install hub items.", "warning");
|
|
884
|
+
await runHubMenu(
|
|
885
|
+
{ select: (title, options) => ctx.ui.select(title, options), confirm: (title, message2) => ctx.ui.confirm(title, message2), notify: (message2, level) => ctx.ui.notify(message2, level) },
|
|
886
|
+
(method, params) => socketCall(connection, method, params ?? {}, 3e5, control)
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
});
|
|
164
890
|
pi.registerCommand("plugins", {
|
|
165
891
|
description: "See what this session loads, and switch a plugin on or off or trust its code",
|
|
166
892
|
handler: async (_args, ctx) => {
|
|
167
893
|
if (!control) return ctx.ui.notify("This session cannot change plugins.", "warning");
|
|
168
894
|
const { projectId } = await socketCall(connection, "plugins.session", {}, 15e3, control).catch(() => ({ projectId: null }));
|
|
169
895
|
await runPluginsMenu(
|
|
170
|
-
{ select: (title, options) => ctx.ui.select(title, options), confirm: (title,
|
|
896
|
+
{ select: (title, options) => ctx.ui.select(title, options), confirm: (title, message2) => ctx.ui.confirm(title, message2), notify: (message2, level) => ctx.ui.notify(message2, level) },
|
|
171
897
|
(method, params) => socketCall(connection, method, params ?? {}, 6e4, control),
|
|
172
898
|
projectId
|
|
173
899
|
);
|
|
174
900
|
}
|
|
175
901
|
});
|
|
902
|
+
pi.registerCommand("accounts", {
|
|
903
|
+
description: "Add, sign in to, check, switch, or remove the Pi accounts this harness uses",
|
|
904
|
+
handler: async (_args, ctx) => {
|
|
905
|
+
if (!control) return ctx.ui.notify("This session cannot manage accounts.", "warning");
|
|
906
|
+
const session = await socketCall(connection, "accounts.session", {}, 15e3, control).catch(() => null);
|
|
907
|
+
if (!session) return ctx.ui.notify("This session has no account to manage.", "warning");
|
|
908
|
+
const outcome = await runAccountsMenu(
|
|
909
|
+
{
|
|
910
|
+
select: (title, options) => ctx.ui.select(title, options),
|
|
911
|
+
confirm: (title, message2) => ctx.ui.confirm(title, message2),
|
|
912
|
+
input: (title, placeholder) => ctx.ui.input(title, placeholder),
|
|
913
|
+
notify: (message2, level) => ctx.ui.notify(message2, level)
|
|
914
|
+
},
|
|
915
|
+
(method, params) => socketCall(connection, method, params ?? {}, 12e4, control),
|
|
916
|
+
session
|
|
917
|
+
);
|
|
918
|
+
if (outcome === "relaunch") ctx.shutdown();
|
|
919
|
+
}
|
|
920
|
+
});
|
|
176
921
|
const { tools } = await socketCall(connection, "tools.list", {}, 15e3);
|
|
177
922
|
stage = "Tool registration";
|
|
178
923
|
for (const tool of tools) {
|
|
@@ -187,6 +932,18 @@ async function piExtension_default(pi) {
|
|
|
187
932
|
}
|
|
188
933
|
});
|
|
189
934
|
}
|
|
935
|
+
const native = await controlCall("terminal.subagents.tools");
|
|
936
|
+
nativeSubagents = native.tools.length > 0;
|
|
937
|
+
for (const tool of native.tools) pi.registerTool({
|
|
938
|
+
name: tool.name,
|
|
939
|
+
label: tool.label,
|
|
940
|
+
description: tool.description,
|
|
941
|
+
parameters: Type.Unsafe(tool.parameters),
|
|
942
|
+
execute: async (_id, input, signal) => {
|
|
943
|
+
const result = await withParentCancellation(signal, () => controlCall("terminal.subagents.call", { name: tool.name, input }, 2147483647));
|
|
944
|
+
return { content: [{ type: "text", text: result.text }], details: result.details ?? {} };
|
|
945
|
+
}
|
|
946
|
+
});
|
|
190
947
|
} catch (error) {
|
|
191
948
|
await terminalCall("startupFailed", { reason: `${stage} failed: ${error instanceof Error ? error.message : "Unavailable"}` }).catch(() => {
|
|
192
949
|
});
|
|
@@ -194,6 +951,7 @@ async function piExtension_default(pi) {
|
|
|
194
951
|
}
|
|
195
952
|
pi.on("session_start", async (_event, ctx) => {
|
|
196
953
|
context = ctx;
|
|
954
|
+
if (timer) clearInterval(timer);
|
|
197
955
|
await terminalCall("ready", { nativeSessionId: ctx.sessionManager.getSessionFile() });
|
|
198
956
|
timer = setInterval(() => {
|
|
199
957
|
if (polling || !context) return;
|
|
@@ -218,13 +976,26 @@ async function piExtension_default(pi) {
|
|
|
218
976
|
usage = emptyUsage();
|
|
219
977
|
reasoningTokens = void 0;
|
|
220
978
|
costReported = false;
|
|
221
|
-
await terminalCall("begin", { text: event.messageType === "user" ? event.prompt : "" });
|
|
979
|
+
const begun = await terminalCall("begin", { text: event.messageType === "user" ? event.prompt : "" });
|
|
980
|
+
if (nativeSubagents) {
|
|
981
|
+
pi.appendEntry(SUBAGENT_OWNER, { runId: begun.runId });
|
|
982
|
+
await controlCall("terminal.subagents.register", { tools: pi.getActiveTools(), model: ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : null, effort: pi.getThinkingLevel(), history: subagentHistory(ctx.sessionManager.getBranch()) });
|
|
983
|
+
subagentParent = true;
|
|
984
|
+
}
|
|
222
985
|
});
|
|
986
|
+
const deliverChildren = async (event, ctx, settle) => {
|
|
987
|
+
if (!nativeSubagents || settle && event.outcome !== "completed" || ctx.signal?.aborted) return;
|
|
988
|
+
const result = await withParentCancellation(ctx.signal, () => controlCall("terminal.subagents.delivery", { history: subagentHistory([...ctx.sessionManager.getBranch(), ...event.entries]), settle }, 2147483647));
|
|
989
|
+
if (!result) return;
|
|
990
|
+
return { entries: [...event.entries, { type: "custom_message", customType: SUBAGENT_RESULTS, content: result.text, display: false, details: { [SUBAGENT_RECEIPT]: result.receipt } }], continue: true };
|
|
991
|
+
};
|
|
992
|
+
pi.on("turn_end", (event, ctx) => deliverChildren(event, ctx, false));
|
|
993
|
+
pi.on("agent_before_settle", (event, ctx) => deliverChildren(event, ctx, true));
|
|
223
994
|
pi.on("message_end", async (event) => {
|
|
224
995
|
if (event.message.role !== "assistant") return;
|
|
225
996
|
if (event.message.stopReason === "error" || event.message.stopReason === "aborted") failure = event.message.errorMessage ?? `Pi stopped: ${event.message.stopReason}`;
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
997
|
+
const text3 = event.message.content.filter((part) => part.type === "text").map((part) => part.text).join("\n");
|
|
998
|
+
if (text3) await terminalCall("message", { text: text3 });
|
|
228
999
|
if (event.message.usage) {
|
|
229
1000
|
const reported = event.message.usage;
|
|
230
1001
|
usage.inputTokens += reported.input ?? 0;
|
|
@@ -269,9 +1040,9 @@ async function piExtension_default(pi) {
|
|
|
269
1040
|
});
|
|
270
1041
|
pi.on("tool_call", async (event) => {
|
|
271
1042
|
try {
|
|
272
|
-
const policy = await
|
|
1043
|
+
const policy = await controlCall("terminal.policy", { tool: event.toolName, args: event.input });
|
|
273
1044
|
if (policy.requiresApproval) {
|
|
274
|
-
const approval = await
|
|
1045
|
+
const approval = await controlCall("terminal.approval", { requestId: event.toolCallId, tool: event.toolName, args: event.input });
|
|
275
1046
|
if (approval.decision === "approved") return;
|
|
276
1047
|
return { block: true, reason: "The user denied this action. Ask how they would like to proceed." };
|
|
277
1048
|
}
|
|
@@ -294,17 +1065,38 @@ async function piExtension_default(pi) {
|
|
|
294
1065
|
fileTools.delete(event.toolCallId);
|
|
295
1066
|
if (!event.isError && path) await terminalCall("file", { path, change: "modified" });
|
|
296
1067
|
});
|
|
297
|
-
pi.on("agent_settled", async () => {
|
|
298
|
-
|
|
1068
|
+
pi.on("agent_settled", async (event, ctx) => {
|
|
1069
|
+
if (subagentParent && !event.aborted && !failure) {
|
|
1070
|
+
try {
|
|
1071
|
+
await controlCall("terminal.subagents.verify", { history: subagentHistory(ctx.sessionManager.getBranch()) }, 2147483647);
|
|
1072
|
+
} catch (error) {
|
|
1073
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
subagentParent = false;
|
|
1077
|
+
await terminalCall("settled", { error: failure, aborted: event.aborted });
|
|
299
1078
|
});
|
|
300
1079
|
pi.on("user_bash", async () => {
|
|
301
1080
|
throw new Error("Use the bash tool so this action can be checked.");
|
|
302
1081
|
});
|
|
303
1082
|
pi.on("session_shutdown", async () => {
|
|
304
1083
|
if (timer) clearInterval(timer);
|
|
1084
|
+
timer = null;
|
|
1085
|
+
context = null;
|
|
305
1086
|
await terminalCall("shutdown").catch(() => {
|
|
306
1087
|
});
|
|
307
1088
|
});
|
|
1089
|
+
const ask = askUserTool({ requestAnswers: (questions) => terminalCall("questions", { questions }, 123e4) });
|
|
1090
|
+
pi.registerTool({
|
|
1091
|
+
name: ask.name,
|
|
1092
|
+
label: ask.label,
|
|
1093
|
+
description: ask.description,
|
|
1094
|
+
parameters: Type.Unsafe(ask.parameters(Type)),
|
|
1095
|
+
async execute(_id, input) {
|
|
1096
|
+
const result = await ask.execute(input);
|
|
1097
|
+
return { content: [{ type: "text", text: typeof result === "string" ? result : result.text }], details: void 0 };
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
308
1100
|
pi.registerTool({
|
|
309
1101
|
name: "ask_user_question",
|
|
310
1102
|
label: "Ask a question",
|