toolroll 0.9.43 → 0.9.45
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 +29 -0
- package/dist/action-ledger.d.ts +5 -2
- package/dist/action-ledger.js +13 -3
- package/dist/cli-http.d.ts +53 -0
- package/dist/cli-http.js +143 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +7 -0
- package/dist/contracts/cli.d.ts +13 -0
- package/dist/contracts/cli.js +7 -0
- package/dist/contracts/gateway-tools.d.ts +82 -0
- package/dist/contracts/gateway-tools.js +33 -0
- package/dist/desktop-update.d.ts +4 -2
- package/dist/desktop-update.js +7 -6
- package/dist/dispatch.d.ts +2 -2
- package/dist/dispatch.js +8 -4
- package/dist/lead-status.d.ts +7 -3
- package/dist/lead-status.js +21 -12
- package/dist/mcp-core.d.ts +110 -0
- package/dist/mcp-core.js +311 -0
- package/dist/mcp-http.d.ts +26 -0
- package/dist/mcp-http.js +151 -0
- package/dist/mcp-person.d.ts +31 -0
- package/dist/mcp-person.js +96 -0
- package/dist/mcp.d.ts +1 -8
- package/dist/mcp.js +21 -322
- package/dist/operate-remote-arguments.d.ts +36 -0
- package/dist/operate-remote-arguments.js +185 -0
- package/dist/operate-remote.d.ts +98 -0
- package/dist/operate-remote.js +145 -0
- package/dist/operate.d.ts +21 -1
- package/dist/operate.js +206 -19
- package/dist/remote-command.d.ts +45 -0
- package/dist/remote-command.js +124 -0
- package/dist/remote-exec.d.ts +18 -0
- package/dist/remote-exec.js +241 -0
- package/dist/remote-run.d.ts +20 -0
- package/dist/remote-run.js +13 -0
- package/dist/scope.js +10 -0
- package/dist/serve.d.ts +7 -0
- package/dist/serve.js +24 -0
- package/dist/store.d.ts +15 -1
- package/dist/store.js +34 -7
- package/dist/surface.d.ts +10 -3
- package/dist/surface.js +67 -2
- package/dist/team-cli.d.ts +19 -0
- package/dist/team-cli.js +20 -3
- package/dist/toolroll-update.js +4 -1
- package/dist/work-summary.d.ts +4 -1
- package/dist/work-summary.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote CLI, the client half (Phase 1). With an API token saved by `toolroll connect`, a command whose contract row
|
|
3
|
+
* says `remote: "yes"` is sent to the central server's `POST /api/cli` and runs there as you; its stdout, stderr and
|
|
4
|
+
* exit code come back byte for byte. `remote: "no"` runs on this computer exactly as before, and `remote: "step-up"`
|
|
5
|
+
* (approvals, people and policy) is refused here: those stay in the console or chat.
|
|
6
|
+
*
|
|
7
|
+
* Nothing here opens the local database or reads a saved owner login; it never prompts, and an uncertain answer to
|
|
8
|
+
* a mutation is never retried.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
11
|
+
import { resolve } from 'node:path';
|
|
12
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
13
|
+
import { envelopeJson } from './envelope.js';
|
|
14
|
+
import { COMMAND_GUIDE } from './surface.js';
|
|
15
|
+
import { centralProfile, UsageError } from './team-cli.js';
|
|
16
|
+
import { CLI_FILES_BYTES, CLI_WAIT_SECONDS } from './cli-http.js';
|
|
17
|
+
import { contractRow, fileArguments, STEP_UP_MESSAGE } from './remote-command.js';
|
|
18
|
+
export { contractRow, STEP_UP_MESSAGE } from './remote-command.js';
|
|
19
|
+
const EXIT = { ok: 0, failed: 1, usage: 2, refused: 3 };
|
|
20
|
+
/** Team commands own their own central path (team-cli.ts). */
|
|
21
|
+
const TEAM_COMMANDS = new Set(['connect', 'lead', 'conversation', 'chat', 'brief']);
|
|
22
|
+
const RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
23
|
+
const REQUEST_TIMEOUT_MS = (CLI_WAIT_SECONDS + 35) * 1_000;
|
|
24
|
+
async function bounded(response) {
|
|
25
|
+
if (!response.body)
|
|
26
|
+
throw new Error('empty');
|
|
27
|
+
const reader = response.body.getReader(), chunks = [];
|
|
28
|
+
let size = 0;
|
|
29
|
+
try {
|
|
30
|
+
while (true) {
|
|
31
|
+
const chunk = await reader.read();
|
|
32
|
+
if (chunk.done)
|
|
33
|
+
break;
|
|
34
|
+
size += chunk.value.byteLength;
|
|
35
|
+
if (size > RESPONSE_BYTES) {
|
|
36
|
+
await reader.cancel();
|
|
37
|
+
throw new Error('too-large');
|
|
38
|
+
}
|
|
39
|
+
chunks.push(chunk.value);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
reader.releaseLock();
|
|
44
|
+
}
|
|
45
|
+
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(chunks)));
|
|
46
|
+
}
|
|
47
|
+
/** One request, never retried. */
|
|
48
|
+
async function post(origin, token, request, options) {
|
|
49
|
+
try {
|
|
50
|
+
const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
|
|
51
|
+
const response = await (options.fetch ?? fetch)(`${origin}/api/cli`, {
|
|
52
|
+
method: 'POST', redirect: 'manual', credentials: 'omit', signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout,
|
|
53
|
+
headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json', accept: 'application/json' }, body: JSON.stringify(request),
|
|
54
|
+
});
|
|
55
|
+
if (response.status >= 300 && response.status < 400) {
|
|
56
|
+
await response.body?.cancel();
|
|
57
|
+
return { kind: 'unconfirmed' };
|
|
58
|
+
}
|
|
59
|
+
const payload = await bounded(response);
|
|
60
|
+
if (payload === null || typeof payload !== 'object' || Array.isArray(payload))
|
|
61
|
+
return { kind: 'unconfirmed' };
|
|
62
|
+
if (response.status === 200 && Number.isInteger(payload.exitCode) && Number(payload.exitCode) >= 0 && Number(payload.exitCode) <= 255 && typeof payload.stdout === 'string' && typeof payload.stderr === 'string') {
|
|
63
|
+
return { kind: 'answer', answer: { exitCode: Number(payload.exitCode), stdout: payload.stdout, stderr: payload.stderr } };
|
|
64
|
+
}
|
|
65
|
+
if (response.status >= 400 && payload.ok === false && typeof payload.code === 'string' && typeof payload.message === 'string') {
|
|
66
|
+
return { kind: 'refused', status: response.status, code: payload.code, message: payload.message.replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '') };
|
|
67
|
+
}
|
|
68
|
+
return { kind: 'unconfirmed' };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return { kind: 'unconfirmed' };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** The declared file arguments' contents, keyed by the argument exactly as typed. argv is never rewritten. */
|
|
75
|
+
function inlineFiles(argv, row, cwd) {
|
|
76
|
+
const paths = fileArguments(argv, row);
|
|
77
|
+
if ('problem' in paths)
|
|
78
|
+
throw new UsageError(paths.problem);
|
|
79
|
+
const files = Object.create(null);
|
|
80
|
+
let size = 0;
|
|
81
|
+
for (const value of paths) {
|
|
82
|
+
if (Object.hasOwn(files, value))
|
|
83
|
+
continue;
|
|
84
|
+
const path = resolve(cwd, value);
|
|
85
|
+
let isFile = false;
|
|
86
|
+
try {
|
|
87
|
+
isFile = statSync(path).isFile();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
isFile = false;
|
|
91
|
+
}
|
|
92
|
+
if (!isFile)
|
|
93
|
+
throw new UsageError('A remote input must be a readable file.');
|
|
94
|
+
let content;
|
|
95
|
+
try {
|
|
96
|
+
content = new TextDecoder('utf-8', { fatal: true }).decode(readFileSync(path));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
throw new UsageError('A remote input must be a readable UTF-8 file.');
|
|
100
|
+
}
|
|
101
|
+
size += Buffer.byteLength(content);
|
|
102
|
+
if (size > CLI_FILES_BYTES)
|
|
103
|
+
throw new UsageError('Files sent with a remote command are limited to 256 KiB in total. Use --local to run it here.');
|
|
104
|
+
files[value] = content;
|
|
105
|
+
}
|
|
106
|
+
return files;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* null: run locally with argv unchanged. `{ local }`: run locally with this argv (`--local` removed).
|
|
110
|
+
* A number: the command ran remotely (or was refused before sending) and this is its exit code.
|
|
111
|
+
*/
|
|
112
|
+
export async function maybeRunRemoteCommand(argv, options = {}) {
|
|
113
|
+
const first = argv[0];
|
|
114
|
+
if (first === undefined || first.startsWith('-') || TEAM_COMMANDS.has(first) || first === 'help')
|
|
115
|
+
return null;
|
|
116
|
+
const out = options.stdout ?? (chunk => { process.stdout.write(chunk); });
|
|
117
|
+
const err = options.stderr ?? (chunk => { process.stderr.write(chunk); });
|
|
118
|
+
const json = argv.includes('--json');
|
|
119
|
+
const lookup = options.modeOf ?? contractRow;
|
|
120
|
+
const row = lookup(argv);
|
|
121
|
+
const command = row?.invocation ?? first;
|
|
122
|
+
const refuse = (reason, message, code) => {
|
|
123
|
+
if (json)
|
|
124
|
+
out(`${envelopeJson({ ok: false, command, reason, message })}\n`);
|
|
125
|
+
else
|
|
126
|
+
err(`${message}\n`);
|
|
127
|
+
return code;
|
|
128
|
+
};
|
|
129
|
+
const profileAt = argv.indexOf('--profile');
|
|
130
|
+
const mode = row?.mode ?? 'no';
|
|
131
|
+
const explicit = profileAt >= 0;
|
|
132
|
+
// Local commands never read the profile, and their own flags (the scan's --local) stay exactly as typed.
|
|
133
|
+
if (mode === 'no' && !explicit)
|
|
134
|
+
return null;
|
|
135
|
+
if (argv.includes('--local')) {
|
|
136
|
+
if (explicit)
|
|
137
|
+
return refuse('usage', '--local cannot be combined with --profile.', EXIT.usage);
|
|
138
|
+
// A row declaring --local keeps its own meaning; elsewhere it only means "run here".
|
|
139
|
+
const declared = COMMAND_GUIDE.find(one => one.invocation === row?.invocation)?.flags?.some(flag => flag.name === 'local') ?? false;
|
|
140
|
+
return { local: declared ? [...argv] : argv.filter(one => one !== '--local') };
|
|
141
|
+
}
|
|
142
|
+
let profile;
|
|
143
|
+
try {
|
|
144
|
+
if (profileAt >= 0 && (argv.filter(one => one === '--profile').length > 1 || !argv[profileAt + 1] || argv[profileAt + 1].startsWith('-')))
|
|
145
|
+
throw new UsageError('Use --profile <name> once.');
|
|
146
|
+
profile = centralProfile(options, profileAt >= 0 ? argv[profileAt + 1] : undefined);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
return refuse('usage', error instanceof UsageError ? error.message : 'The remote profile cannot be read.', EXIT.usage);
|
|
150
|
+
}
|
|
151
|
+
// No saved token: today's local behaviour. A profile saved with a password serves chat only.
|
|
152
|
+
if (profile === null || !profile.apiToken) {
|
|
153
|
+
if (!explicit)
|
|
154
|
+
return null;
|
|
155
|
+
return refuse('usage', 'This profile has no API token. Connect again with your so_ token (toolroll connect <origin> --token-stdin).', EXIT.usage);
|
|
156
|
+
}
|
|
157
|
+
if (row === null || mode === 'no') {
|
|
158
|
+
return refuse('usage', `${command} runs only on this computer. Drop --profile to run it here.`, EXIT.usage);
|
|
159
|
+
}
|
|
160
|
+
if (mode === 'step-up')
|
|
161
|
+
return refuse('step-up', STEP_UP_MESSAGE, EXIT.refused);
|
|
162
|
+
const sent = explicit ? argv.filter((_one, index) => index !== profileAt && index !== profileAt + 1) : [...argv];
|
|
163
|
+
let files;
|
|
164
|
+
try {
|
|
165
|
+
files = inlineFiles(sent, row, options.cwd ?? process.cwd());
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
return refuse('usage', error instanceof UsageError ? error.message : 'A file could not be read.', EXIT.usage);
|
|
169
|
+
}
|
|
170
|
+
const deliver = (outcome) => {
|
|
171
|
+
if (outcome.kind === 'answer') {
|
|
172
|
+
// Byte for byte. --json answers are also captured so -o writes the same envelope a local run would.
|
|
173
|
+
if (json) {
|
|
174
|
+
try {
|
|
175
|
+
const parsed = JSON.parse(outcome.answer.stdout);
|
|
176
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
177
|
+
envelopeJson(parsed);
|
|
178
|
+
}
|
|
179
|
+
catch { /* not one envelope */ }
|
|
180
|
+
}
|
|
181
|
+
if (outcome.answer.stdout)
|
|
182
|
+
out(outcome.answer.stdout);
|
|
183
|
+
if (outcome.answer.stderr)
|
|
184
|
+
err(outcome.answer.stderr);
|
|
185
|
+
return outcome.answer.exitCode;
|
|
186
|
+
}
|
|
187
|
+
if (outcome.kind === 'refused')
|
|
188
|
+
return refuse(outcome.code, outcome.message, outcome.status === 401 || outcome.status === 403 || outcome.status === 501 ? EXIT.refused : outcome.status < 500 ? EXIT.usage : EXIT.failed);
|
|
189
|
+
return refuse('unconfirmed', row?.mutation === 'none'
|
|
190
|
+
? `${profile.origin} could not be reached or answered unexpectedly.`
|
|
191
|
+
: `The answer from ${profile.origin} could not be confirmed. Check the task's state before running this again; it was not retried.`, EXIT.failed);
|
|
192
|
+
};
|
|
193
|
+
if (row?.invocation === 'task wait')
|
|
194
|
+
return waitRemotely(sent, files, profile, options, deliver);
|
|
195
|
+
return deliver(await post(profile.origin, profile.token, { argv: sent, files }, options));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* `task wait` holds the server for at most CLI_WAIT_SECONDS per request; the client keeps asking within the caller's
|
|
199
|
+
* own --timeout (none: until the task settles or the wait is interrupted). The final answer is the server's own.
|
|
200
|
+
*/
|
|
201
|
+
async function waitRemotely(argv, files, profile, options, deliver) {
|
|
202
|
+
const at = argv.indexOf('--timeout');
|
|
203
|
+
const given = at < 0 ? null : Number(argv[at + 1]);
|
|
204
|
+
const base = at < 0 ? argv : argv.filter((_one, index) => index !== at && index !== at + 1);
|
|
205
|
+
if (given !== null && (!Number.isFinite(given) || given < 0))
|
|
206
|
+
return deliver(await post(profile.origin, profile.token, { argv, files }, options));
|
|
207
|
+
const json = base.includes('--json');
|
|
208
|
+
const probe = json ? base : [...base, '--json'];
|
|
209
|
+
const started = Date.now();
|
|
210
|
+
const sleep = options.sleep ?? ((ms, signal) => delay(ms, undefined, { signal }));
|
|
211
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
212
|
+
while (true) {
|
|
213
|
+
const remaining = given === null ? CLI_WAIT_SECONDS : Math.max(0, given - (Date.now() - started) / 1_000);
|
|
214
|
+
const slice = Math.min(CLI_WAIT_SECONDS, Math.floor(remaining * 1_000) / 1_000);
|
|
215
|
+
const outcome = await post(profile.origin, profile.token, { argv: [...probe, '--timeout', String(slice)], files }, options);
|
|
216
|
+
if (outcome.kind !== 'answer')
|
|
217
|
+
return deliver(outcome);
|
|
218
|
+
let reason;
|
|
219
|
+
try {
|
|
220
|
+
reason = JSON.parse(outcome.answer.stdout).reason;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return deliver(outcome);
|
|
224
|
+
}
|
|
225
|
+
const timedOut = reason === 'timeout';
|
|
226
|
+
if (!timedOut || slice <= 0 || given !== null && remaining - slice <= 0) {
|
|
227
|
+
if (json)
|
|
228
|
+
return deliver(outcome);
|
|
229
|
+
// Settled (or out of time): the server renders the final answer in the person's own format.
|
|
230
|
+
return deliver(await post(profile.origin, profile.token, { argv: [...base, '--timeout', '0'], files }, options));
|
|
231
|
+
}
|
|
232
|
+
if (signal.aborted)
|
|
233
|
+
return deliver(outcome);
|
|
234
|
+
try {
|
|
235
|
+
await sleep(250, signal);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return deliver(outcome);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** A principal re-proved against the store for one remote run. */
|
|
2
|
+
export type RemoteRun = {
|
|
3
|
+
readonly account: string;
|
|
4
|
+
readonly scope: "read" | "act";
|
|
5
|
+
readonly tokenName: string;
|
|
6
|
+
readonly source: "api" | "mcp";
|
|
7
|
+
/** What credential lookups hand back for this person in place of a password: unguessable, this run only. */
|
|
8
|
+
readonly secret: string;
|
|
9
|
+
/** Whether the person may use `repo` now (their token's projects and their account's current access). */
|
|
10
|
+
readonly allows: (repo: string | null) => boolean;
|
|
11
|
+
/** The projects listings and task references may disclose (remoteLensOf); null shows every project. */
|
|
12
|
+
readonly lens: readonly string[] | null;
|
|
13
|
+
/** Called with the task a remote `task add` filed (or replayed), so its action history names it. */
|
|
14
|
+
readonly noteTask: (taskId: string) => void;
|
|
15
|
+
/** Files the caller sent, keyed by the argument that names them. Nothing else is read for input. */
|
|
16
|
+
readonly files: Readonly<Record<string, string>>;
|
|
17
|
+
};
|
|
18
|
+
/** The remote run this code is inside, or null for every local command. */
|
|
19
|
+
export declare function activeRemote(): RemoteRun | null;
|
|
20
|
+
export declare function withRemote<T>(run: RemoteRun, fn: () => T): T;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The remote run this code is inside (runOperateAs in operate.ts), if any. A leaf module so the credential
|
|
3
|
+
* checks every command reaches (scope.ts) can honour it without importing the command table.
|
|
4
|
+
*/
|
|
5
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
|
+
const running = new AsyncLocalStorage();
|
|
7
|
+
/** The remote run this code is inside, or null for every local command. */
|
|
8
|
+
export function activeRemote() {
|
|
9
|
+
return running.getStore() ?? null;
|
|
10
|
+
}
|
|
11
|
+
export function withRemote(run, fn) {
|
|
12
|
+
return running.run(run, fn);
|
|
13
|
+
}
|
package/dist/scope.js
CHANGED
|
@@ -4,6 +4,7 @@ import { passwordGuardOf } from "./sign-in-guard.js";
|
|
|
4
4
|
import { validateScopeText } from "./task-text.js";
|
|
5
5
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
6
6
|
import { projectAuthority } from "./project-access.js";
|
|
7
|
+
import { activeRemote } from "./remote-run.js";
|
|
7
8
|
import { claimActor, currentActor, parseLeadToken } from "./actor.js";
|
|
8
9
|
/**
|
|
9
10
|
* What a task is allowed to become, agreed before anything builds it.
|
|
@@ -707,6 +708,15 @@ password) {
|
|
|
707
708
|
export function authenticateAccount(store, by, secret) {
|
|
708
709
|
if (store.listApprovers().length === 0)
|
|
709
710
|
return { ok: false, reason: "no-approvers" };
|
|
711
|
+
// A remote run (runOperateAs) is its token's person: only that run's own credential names them, no password is
|
|
712
|
+
// checked or counted, and a read token stands as a viewer.
|
|
713
|
+
const remote = activeRemote();
|
|
714
|
+
if (remote !== null) {
|
|
715
|
+
const account = by === remote.account && secret === remote.secret ? store.accountOf(by) : null;
|
|
716
|
+
if (account === null || account.revokedAt !== null)
|
|
717
|
+
return { ok: false, reason: "unknown" };
|
|
718
|
+
return { ok: true, role: remote.scope === "act" ? account.role : "viewer", generation: account.generation };
|
|
719
|
+
}
|
|
710
720
|
// A lead token signs in as its owner, only inside a command the lead runs with it (never a console sign-in).
|
|
711
721
|
const acting = currentActor();
|
|
712
722
|
if (acting?.lead === true && parseLeadToken(secret) !== null) {
|
package/dist/serve.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { CodingWorkspace } from './coding-workspace.js';
|
|
|
3
3
|
import { type FetchLike } from "./flow-share.js";
|
|
4
4
|
import { type MailSender } from "./flow-actions.js";
|
|
5
5
|
import { type TriggerIo } from "./flow-triggers.js";
|
|
6
|
+
import { type CliHttpOptions, type RunOperateAs } from './cli-http.js';
|
|
6
7
|
import { type AssignmentSnapshot } from './assignment.js';
|
|
7
8
|
import type { PublishExec } from './publish.js';
|
|
8
9
|
import { type TaskStatus } from './task-status.js';
|
|
@@ -24,6 +25,8 @@ import type { RevisionLineage } from "./store.js";
|
|
|
24
25
|
import type { SubscriptionMateRunner } from "./subscription-chat.js";
|
|
25
26
|
import { type InstallMethod } from "./install-method.js";
|
|
26
27
|
export type ServeOptions = {
|
|
28
|
+
/** Tests: runs a person's command for the HTTP MCP gateway instead of operate.ts's. */
|
|
29
|
+
runOperateAs?: import("./mcp-person.js").RunOperateAs;
|
|
27
30
|
/** `toolroll demo`: the scripted lead that answers Chat instead of a model. */
|
|
28
31
|
demoLead?: import("./demo.js").DemoLead;
|
|
29
32
|
/** Tests: the bin whose real path says how Toolroll was installed (Settings → Updates' command). */
|
|
@@ -41,6 +44,10 @@ export type ServeOptions = {
|
|
|
41
44
|
* install/push cards light up. X-Forwarded-* is never consulted.
|
|
42
45
|
*/
|
|
43
46
|
publicUrl?: string;
|
|
47
|
+
/** Tests: the shared command boundary `POST /api/cli` runs (default: operate.ts's runOperateAs). */
|
|
48
|
+
cliRunner?: RunOperateAs;
|
|
49
|
+
/** Tests: remote command metadata until the shared contract declares it. */
|
|
50
|
+
cliModeOf?: CliHttpOptions['modeOf'];
|
|
44
51
|
/** Where repos.json lives — every enrollment locks exactly this file. */
|
|
45
52
|
registryPath?: string;
|
|
46
53
|
/** This console fronts an `up` process: onboarding copy says how to watch. */
|
package/dist/serve.js
CHANGED
|
@@ -67,6 +67,7 @@ import { learningHtml } from "./workspace-ui.js";
|
|
|
67
67
|
import { acceptWithChecksOf, resultActsOf } from "./result-acts.js";
|
|
68
68
|
import { createSessionEndpoint } from './session-server.js';
|
|
69
69
|
import { handleTeamHttp } from './team-http.js';
|
|
70
|
+
import { handleCliHttp } from './cli-http.js';
|
|
70
71
|
import { teamWorkspaceHtml } from './team-ui.js';
|
|
71
72
|
import { createTeamRuntime } from './team-runtime.js';
|
|
72
73
|
import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-revision.js";
|
|
@@ -262,6 +263,7 @@ import { budgetHoldWords, budgetLabel, budgetStates, monthNamed, monthOf, spendI
|
|
|
262
263
|
import { targetOf } from "./monitoring.js";
|
|
263
264
|
import { SSO_CSS, ssoSettingsHtml } from "./sso-ui.js";
|
|
264
265
|
import { mintApiToken, parseApiToken, secretMatches, TOKEN_DAYS } from "./api-tokens.js";
|
|
266
|
+
import { createMcpHttp } from "./mcp-http.js";
|
|
265
267
|
import { CREDENTIALS_CSS, credentialsHtml, tokenShownHtml } from "./credentials-ui.js";
|
|
266
268
|
import { logEvent } from "./log.js";
|
|
267
269
|
import { modeTermsFromJson, modeWords, presetTerms, modeTermsJson, modeDigestOf, MODE_MAX_DAYS } from "./modes.js";
|
|
@@ -795,6 +797,21 @@ export function createDecisionServer(options) {
|
|
|
795
797
|
return who?.via === 'cookie' ? teamBrowserReply(reply, actor, who.session.csrf) : reply;
|
|
796
798
|
}, cursor: team.cursor, streams: teamStreams,
|
|
797
799
|
});
|
|
800
|
+
// Remote CLI: one live API token names the person; the shared command boundary decides everything else.
|
|
801
|
+
const cliEndpoint = (request, response) => handleCliHttp(request, response, {
|
|
802
|
+
authenticate: request => {
|
|
803
|
+
const who = identify(request, false);
|
|
804
|
+
const id = parseApiToken(/^Bearer (so_\S+)$/.exec(request.headers.authorization ?? "")?.[1] ?? "")?.id;
|
|
805
|
+
const row = id === undefined ? undefined : store.apiTokenSecret(id)?.row;
|
|
806
|
+
const account = who === null ? null : store.accountOf(who.name);
|
|
807
|
+
if (who?.via !== "bearer" || who.token === undefined || row === undefined || row.account !== who.name || account === null || account.revokedAt !== null)
|
|
808
|
+
return null;
|
|
809
|
+
return { kind: "person", account: who.name, generation: account.generation, scope: row.access, tokenId: row.id, projects: account.projects === null ? null : [...account.projects] };
|
|
810
|
+
},
|
|
811
|
+
run: async () => options.cliRunner ?? (await import("./operate.js")).runOperateAs ?? null,
|
|
812
|
+
modeOf: options.cliModeOf,
|
|
813
|
+
store,
|
|
814
|
+
});
|
|
798
815
|
const sessionEndpoint = createSessionEndpoint({ store, workspace: coding, projects: codingProjects, projectAllowed: repo => rowVisible(liveCeiling(), repo) });
|
|
799
816
|
/** The enumerable admission list for roll-up SQL: repos-only ceilings
|
|
800
817
|
* enumerate themselves; root ceilings enumerate the STORED repos that
|
|
@@ -1226,6 +1243,9 @@ export function createDecisionServer(options) {
|
|
|
1226
1243
|
if (url.searchParams.has("token")) {
|
|
1227
1244
|
return respond(response, 400, "text/plain; charset=utf-8", "credentials never travel in URLs");
|
|
1228
1245
|
}
|
|
1246
|
+
// The MCP gateway over HTTP (mcp-http.ts): coordinators and people's own API tokens, after the Host check.
|
|
1247
|
+
if (url.pathname === "/mcp")
|
|
1248
|
+
return mcpHttp(request, response);
|
|
1229
1249
|
// Back from Google's consent screen (v89). The session cookie is SameSite=Strict and stays behind on
|
|
1230
1250
|
// a return from another site: the visit's one-time state (made by a signed-in approver) is the proof.
|
|
1231
1251
|
if (url.pathname === GOOGLE_CALLBACK && request.method === "GET")
|
|
@@ -1237,6 +1257,8 @@ export function createDecisionServer(options) {
|
|
|
1237
1257
|
return;
|
|
1238
1258
|
if (await teamEndpoint(request, response))
|
|
1239
1259
|
return;
|
|
1260
|
+
if (await cliEndpoint(request, response))
|
|
1261
|
+
return;
|
|
1240
1262
|
if (options.configDir !== undefined && await handleTeamsHttp(request, response, { store, dir: options.configDir, ...(options.teamsFetcher ? { fetcher: options.teamsFetcher } : {}), clock }))
|
|
1241
1263
|
return;
|
|
1242
1264
|
if (serveBrowserAsset(request, response, url.pathname))
|
|
@@ -11758,6 +11780,8 @@ export function createDecisionServer(options) {
|
|
|
11758
11780
|
approved: (() => { const scope = store.getScope(taskId); return scope !== null && approvalOf(scope).approved; })(),
|
|
11759
11781
|
}));
|
|
11760
11782
|
}
|
|
11783
|
+
const mcpHttp = createMcpHttp({ store, clock, evidenceRoot, signedIn: request => identify(request) !== null,
|
|
11784
|
+
enrolled: () => [...new Set([...managedRepos(), ...store.listProjects().map(project => project.path)])], ...(options.runOperateAs === undefined ? {} : { runAs: options.runOperateAs }) });
|
|
11761
11785
|
// ---- identity ------------------------------------------------------------
|
|
11762
11786
|
function identify(request, touch = true) {
|
|
11763
11787
|
// v101: an API token. Wrong ones spend the address's tries like a wrong password; expired or revoked ones, and a removed account's, name no one.
|
package/dist/store.d.ts
CHANGED
|
@@ -145,7 +145,21 @@ export type SealedLedgerEntry = LedgerEntry & {
|
|
|
145
145
|
hash: string;
|
|
146
146
|
} | null;
|
|
147
147
|
};
|
|
148
|
-
export declare const SCHEMA_VERSION =
|
|
148
|
+
export declare const SCHEMA_VERSION = 110;
|
|
149
|
+
/**
|
|
150
|
+
* The migrations `toolroll update` may carry a database through in place: each
|
|
151
|
+
* one only adds (a table, a column, an accepted value) and leaves every saved row
|
|
152
|
+
* as it was, so the updater's rehearsal on a copy (same history, integrity ok)
|
|
153
|
+
* proves it and the verified backup undoes it. Any migration not listed here
|
|
154
|
+
* needs the separate verified migration procedure. The desktop update never
|
|
155
|
+
* migrates and does not read this list.
|
|
156
|
+
*
|
|
157
|
+
* v110 rebuilds action_ledger only to widen its source check ('api', 'mcp'):
|
|
158
|
+
* every row, id and hash-chain link is copied unchanged.
|
|
159
|
+
*/
|
|
160
|
+
export declare const UPDATE_SAFE_MIGRATIONS: readonly number[];
|
|
161
|
+
/** Whether a database settled at `version` reaches this build's schema through update-safe migrations alone. */
|
|
162
|
+
export declare function updateSafeSchema(version: number | null): boolean;
|
|
149
163
|
export type ApiTokenRow = {
|
|
150
164
|
id: string;
|
|
151
165
|
account: string;
|
package/dist/store.js
CHANGED
|
@@ -64,7 +64,7 @@ import { authenticate as runnerAuthenticate } from "./runner.js";
|
|
|
64
64
|
import { stampNewProjectLevel } from "./check-levels.js";
|
|
65
65
|
import { parseCheckProgressSnapshot } from "./check-progress.js";
|
|
66
66
|
import { normalizeProjectAccess, projectAccessAllows, readProjectAccess } from "./project-access.js";
|
|
67
|
-
import { LEDGER_SCHEMA, LEDGER_TABLE, LEDGER_V54_COLUMNS, LEDGER_V54_TABLE, installLedgerTriggers } from "./action-ledger.js";
|
|
67
|
+
import { LEDGER_SCHEMA, LEDGER_TABLE, LEDGER_V54_COLUMNS, LEDGER_V54_TABLE, LEDGER_V99_TABLE, installLedgerTriggers } from "./action-ledger.js";
|
|
68
68
|
import { PLAN_AUTO_SCHEMA } from "./plan-auto.js";
|
|
69
69
|
import { RECIPE_SCHEMA } from "./recipes.js";
|
|
70
70
|
import { POLICY_SCHEMA, agentRefusal, approvalRefusal, attendedRefusal, policyParts, readPolicy, underCeiling, sessionCeilingRefusal } from "./policy.js";
|
|
@@ -563,7 +563,29 @@ const REGISTERED_AT_FILING = "again, as it was filed";
|
|
|
563
563
|
// v107 keeps a check's progress and result in one record (run_check absorbs check_progress).
|
|
564
564
|
// v108 keeps sign-in pauses: one incident per provider whose sign-in stopped working (provider_auth_pause).
|
|
565
565
|
// v109 remembers which sign-in pause parked a task (task_ref.auth_wait_pause), so reviews and other runs outside the tick wait for it too.
|
|
566
|
-
|
|
566
|
+
// v110 lets the action ledger record commands a person ran on this server with their API token (source 'api', or 'mcp' from their agent).
|
|
567
|
+
export const SCHEMA_VERSION = 110;
|
|
568
|
+
/**
|
|
569
|
+
* The migrations `toolroll update` may carry a database through in place: each
|
|
570
|
+
* one only adds (a table, a column, an accepted value) and leaves every saved row
|
|
571
|
+
* as it was, so the updater's rehearsal on a copy (same history, integrity ok)
|
|
572
|
+
* proves it and the verified backup undoes it. Any migration not listed here
|
|
573
|
+
* needs the separate verified migration procedure. The desktop update never
|
|
574
|
+
* migrates and does not read this list.
|
|
575
|
+
*
|
|
576
|
+
* v110 rebuilds action_ledger only to widen its source check ('api', 'mcp'):
|
|
577
|
+
* every row, id and hash-chain link is copied unchanged.
|
|
578
|
+
*/
|
|
579
|
+
export const UPDATE_SAFE_MIGRATIONS = Object.freeze([110]);
|
|
580
|
+
/** Whether a database settled at `version` reaches this build's schema through update-safe migrations alone. */
|
|
581
|
+
export function updateSafeSchema(version) {
|
|
582
|
+
if (version === null || !Number.isSafeInteger(version) || version <= 0 || version > SCHEMA_VERSION)
|
|
583
|
+
return false;
|
|
584
|
+
for (let next = version + 1; next <= SCHEMA_VERSION; next++)
|
|
585
|
+
if (!UPDATE_SAFE_MIGRATIONS.includes(next))
|
|
586
|
+
return false;
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
567
589
|
/** v102: a project's approval rules, and each person's approval of an exact scope (two are needed for protected work). */
|
|
568
590
|
const APPROVAL_SCHEMA = `
|
|
569
591
|
CREATE TABLE IF NOT EXISTS approval_policy (
|
|
@@ -5212,8 +5234,8 @@ function migrate(db, origin) {
|
|
|
5212
5234
|
tool_call INTEGER REFERENCES teammate_call(id),
|
|
5213
5235
|
suggestion INTEGER REFERENCES teammate_suggestion(id)
|
|
5214
5236
|
)`, ["id", "teammate", "card", "entry", "question", "options_json", "asked_of", "state", "choice", "answer", "answered_by", "answered_via", "answered_at", "created_at", "tool_call", "suggestion"]);
|
|
5215
|
-
// v99: the action ledger admits sign-in and policy events, each with a short detail.
|
|
5216
|
-
|
|
5237
|
+
// v99: the action ledger admits sign-in and policy events, each with a short detail; v110: and remote commands ('api', 'mcp').
|
|
5238
|
+
rebuildLedger(db);
|
|
5217
5239
|
// v101: a coordinator credential's expiry (null: made before expiry existed, until renewed).
|
|
5218
5240
|
addColumn(db, "coordinator_credential", "expires_at", "TEXT");
|
|
5219
5241
|
// v102: who filed a task (a person, or the person a coordinator acts for; null when no person is known) and what kind of filer.
|
|
@@ -5534,19 +5556,24 @@ function readWebSession(row) {
|
|
|
5534
5556
|
}
|
|
5535
5557
|
/**
|
|
5536
5558
|
* v99: the ledger's source admits 'sign-in' and 'policy', and each row may
|
|
5537
|
-
* carry a short detail.
|
|
5559
|
+
* carry a short detail. v110: it admits 'api' and 'mcp' too, from either
|
|
5560
|
+
* earlier shape; rows, ids and the counter are carried whole, so the v103
|
|
5561
|
+
* seals over them still hold. The work triggers on other tables write into the
|
|
5538
5562
|
* ledger, and a rename refuses while a trigger points at a table that's gone,
|
|
5539
5563
|
* so every trigger naming it is dropped first; openStore puts them back
|
|
5540
5564
|
* (LEDGER_SCHEMA and installLedgerTriggers run after migration).
|
|
5541
5565
|
*/
|
|
5542
|
-
function
|
|
5566
|
+
function rebuildLedger(db) {
|
|
5543
5567
|
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'action_ledger'").get();
|
|
5544
5568
|
if (row === undefined || canonicalDdl(String(row["sql"])) === canonicalDdl(LEDGER_TABLE("action_ledger")))
|
|
5545
5569
|
return;
|
|
5546
5570
|
for (const trigger of db.prepare("SELECT name FROM sqlite_master WHERE type = 'trigger' AND sql LIKE '%action_ledger%'").all()) {
|
|
5547
5571
|
db.exec(`DROP TRIGGER IF EXISTS "${String(trigger["name"]).replace(/"/g, '""')}"`);
|
|
5548
5572
|
}
|
|
5549
|
-
|
|
5573
|
+
if (canonicalDdl(String(row["sql"])) === canonicalDdl(LEDGER_V54_TABLE("action_ledger")))
|
|
5574
|
+
rebuildExact(db, "action_ledger", LEDGER_V54_TABLE, LEDGER_TABLE, LEDGER_V54_COLUMNS);
|
|
5575
|
+
else
|
|
5576
|
+
rebuildExact(db, "action_ledger", LEDGER_V99_TABLE, LEDGER_TABLE, [...LEDGER_V54_COLUMNS, "detail"]);
|
|
5550
5577
|
}
|
|
5551
5578
|
/** v34: artifact.kind admits 'report'. A table already rebuilt to the v38
|
|
5552
5579
|
* shape (migrate() runs unconditionally on every open, including a fresh
|
package/dist/surface.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type CommandFlag, type CommandRow } from './contracts/cli.js';
|
|
1
|
+
import { type CommandFlag, type CommandRow, type RemotePolicy } from './contracts/cli.js';
|
|
2
2
|
/** A guide row and its flags are derived from their schemas in src/contracts/cli.ts. */
|
|
3
|
-
export type { CommandFlag, CommandRow };
|
|
3
|
+
export type { CommandFlag, CommandRow, RemotePolicy };
|
|
4
4
|
/** The guide's limits, stated machine-readably in every dump. */
|
|
5
5
|
export declare const SURFACE_NOTES: {
|
|
6
6
|
readonly authority: "documentation is not permission; session descriptors also drive parser and request validation; legacy entries remain advisory";
|
|
@@ -9,7 +9,14 @@ export declare const SURFACE_NOTES: {
|
|
|
9
9
|
readonly database: "local database commands wait up to 15 seconds for another writer; an exhausted wait returns database-busy and names the selected file";
|
|
10
10
|
};
|
|
11
11
|
export declare const SURFACE_SCHEMA_VERSION = 1;
|
|
12
|
-
/** Every
|
|
12
|
+
/** Every row's remote policy, by invocation. Exported for the contract test, which holds it to the guide exactly. */
|
|
13
|
+
export declare const REMOTE_POLICY: ReadonlyMap<string, RemotePolicy>;
|
|
14
|
+
/** How many times each invocation was classified — the contract test refuses a row stated twice. */
|
|
15
|
+
export declare const REMOTE_POLICY_COUNT: number;
|
|
16
|
+
/** Every declared command, in guide order, paired with the schema of its `--json` answer (src/contracts/cli.ts).
|
|
17
|
+
* An unclassified row is "no": nothing runs remotely by omission. */
|
|
13
18
|
export declare const COMMAND_ENTRIES: readonly import("./contracts/cli.js").CommandEntry[];
|
|
19
|
+
/** The invocations the guide states, for the contract test (a classification naming none of them is stale). */
|
|
20
|
+
export declare const GUIDE_INVOCATIONS: readonly string[];
|
|
14
21
|
/** The guide `contract --commands` dumps: each declared command's row, projected from COMMAND_ENTRIES. */
|
|
15
22
|
export declare const COMMAND_GUIDE: readonly CommandRow[];
|
package/dist/surface.js
CHANGED
|
@@ -416,7 +416,72 @@ const GUIDE_ROWS = [
|
|
|
416
416
|
operator("onboard", "add this repository, report signed-in agents, install or --remove the operator skill (--yes), offer the starter flows (--starter <ids>), and print the handoff"),
|
|
417
417
|
operator("daemon", "install the loop under the OS service manager"),
|
|
418
418
|
];
|
|
419
|
-
/**
|
|
420
|
-
|
|
419
|
+
/**
|
|
420
|
+
* Whether each row may run on a central server for a person signed in with their own API token (`runOperateAs` in
|
|
421
|
+
* operate.ts), stated for every row by name. "no" is this machine's own infrastructure, credentials, files and the
|
|
422
|
+
* commands that only make sense beside a checkout (or that the local CLI answers itself); "step-up" is approvals,
|
|
423
|
+
* people and policy, which stay in the console and chat; "yes" runs as the person, within their token's scope and
|
|
424
|
+
* project access. A row missing here fails the contract test, and runs nowhere remotely meanwhile.
|
|
425
|
+
*/
|
|
426
|
+
const REMOTE_NO = [
|
|
427
|
+
// Answered by the local CLI, or a client of a central server itself.
|
|
428
|
+
"", "pulls", "graph", "repos", "repos add", "repos remove", "repos add-from-github", "contract", "skills list", "skills get", "skills install",
|
|
429
|
+
"link", "unlink", "update", "demo", "chat", "connect", "lead list", "lead create", "lead update", "lead member", "lead transfer",
|
|
430
|
+
"conversation list", "conversation create", "conversation show", "conversation member", "conversation edit", "conversation withdraw",
|
|
431
|
+
"conversation read", "conversation follow", "conversation stop",
|
|
432
|
+
"session capabilities", "session list", "session show", "session changes", "session start", "session send", "session stop", "session resume", "session recover",
|
|
433
|
+
// This machine's checkout, saved profile, credentials and catch-up.
|
|
434
|
+
"project show", "project use", "brief", "lead token", "lead say",
|
|
435
|
+
"assignment show", "assignment updates", "assignment claim", "assignment check", "assignment brief", "assignment inbox", "assignment ack",
|
|
436
|
+
// Workers, the loop and the services that run here.
|
|
437
|
+
"claim", "heartbeat", "release", "reap", "tick", "build", "reconcile",
|
|
438
|
+
"runner register", "runner retire", "runner bind", "runner capacity", "coordinator mint", "coordinator revoke", "mcp", "outbox deliver", "peek",
|
|
439
|
+
"serve", "watch", "up", "onboard", "daemon", "bridge", "setup show", "setup clear",
|
|
440
|
+
// Provider keys and model tools on this machine.
|
|
441
|
+
"providers", "keys status", "keys set", "keys clear", "keys verify", "keys auth", "models check", "models update", "models watch",
|
|
442
|
+
// Publication and release.
|
|
443
|
+
"publish", "publish status", "publish merge", "publish refire", "publish unblock", "publish rearm", "publish grant", "publish revoke",
|
|
444
|
+
// A repository's authority grants on this machine.
|
|
445
|
+
"enroll", "revoke",
|
|
446
|
+
// Flow steps that fetch addresses or run checks and scripts here.
|
|
447
|
+
"flows import", "flows trigger check", "flows script save",
|
|
448
|
+
// Reads this machine owner's own Claude and Codex session files (~/.claude, ~/.codex).
|
|
449
|
+
"memory propose",
|
|
450
|
+
];
|
|
451
|
+
const REMOTE_STEP_UP = [
|
|
452
|
+
"proposals", "decide", "task regate", "task approve", "task accept", "task merge", "run settle", "routine approve", "knowledge apply", "memory apply",
|
|
453
|
+
"flows card approve", "flows card send-back", "flows script approve",
|
|
454
|
+
"approver list", "approver add", "people list", "people invite", "people projects", "people revoke",
|
|
455
|
+
"mode show", "mode set", "mode revoke", "chat-approval on", "review on", "review off", "config set", "config clear",
|
|
456
|
+
"verify set", "verify clear", "verify level", "project checks", "project concurrency", "intake grant", "intake clear", "webhook primary",
|
|
457
|
+
];
|
|
458
|
+
const REMOTE_YES = [
|
|
459
|
+
"status", "integrations", "ready", "gaps", "grants", "sync",
|
|
460
|
+
"task add", "task ask", "task checks", "task add-tests", "task list", "task show", "check-progress", "task wait", "task complete", "task revise",
|
|
461
|
+
"task state", "task block", "task unblock", "task next", "task steer", "task assign", "task scope", "task plan", "task hold", "task unhold", "task require",
|
|
462
|
+
"task requeue", "task review", "task repair", "task route", "task reopen", "task stop", "task resume",
|
|
463
|
+
"runner list", "coordinator list", "cap list", "cap add", "outbox list", "incident list", "incident resolve",
|
|
464
|
+
"routine list", "routine show", "routine add", "routine refresh", "routine pause", "routine resume", "routine run-now",
|
|
465
|
+
"config show", "verify show", "intake show", "intake run", "intake preview", "intake pr-comments", "template list", "template show",
|
|
466
|
+
"contest show", "contest exclude", "webhook status", "webhook test", "review show", "chat-approval show", "chat-approval off",
|
|
467
|
+
"knowledge search", "knowledge impact", "knowledge refresh",
|
|
468
|
+
"memory search", "memory decisions", "memory show", "memory decide", "memory retire", "memory review", "memory status",
|
|
469
|
+
"flows list", "flows show", "flows export", "flows create", "flows edit", "flows trigger add", "flows trigger pause", "flows trigger resume",
|
|
470
|
+
"flows trigger remove", "flows card add", "flows archive",
|
|
471
|
+
"models status", "models list",
|
|
472
|
+
];
|
|
473
|
+
/** Every row's remote policy, by invocation. Exported for the contract test, which holds it to the guide exactly. */
|
|
474
|
+
export const REMOTE_POLICY = new Map([
|
|
475
|
+
...REMOTE_NO.map(invocation => [invocation, "no"]),
|
|
476
|
+
...REMOTE_STEP_UP.map(invocation => [invocation, "step-up"]),
|
|
477
|
+
...REMOTE_YES.map(invocation => [invocation, "yes"]),
|
|
478
|
+
]);
|
|
479
|
+
/** How many times each invocation was classified — the contract test refuses a row stated twice. */
|
|
480
|
+
export const REMOTE_POLICY_COUNT = REMOTE_NO.length + REMOTE_STEP_UP.length + REMOTE_YES.length;
|
|
481
|
+
/** Every declared command, in guide order, paired with the schema of its `--json` answer (src/contracts/cli.ts).
|
|
482
|
+
* An unclassified row is "no": nothing runs remotely by omission. */
|
|
483
|
+
export const COMMAND_ENTRIES = commandEntries(GUIDE_ROWS.map(row => ({ ...row, remote: REMOTE_POLICY.get(row.invocation) ?? "no" })));
|
|
484
|
+
/** The invocations the guide states, for the contract test (a classification naming none of them is stale). */
|
|
485
|
+
export const GUIDE_INVOCATIONS = GUIDE_ROWS.map(row => row.invocation);
|
|
421
486
|
/** The guide `contract --commands` dumps: each declared command's row, projected from COMMAND_ENTRIES. */
|
|
422
487
|
export const COMMAND_GUIDE = COMMAND_ENTRIES.map(entry => entry.guide);
|
package/dist/team-cli.d.ts
CHANGED
|
@@ -11,5 +11,24 @@ export type TeamCliOptions = {
|
|
|
11
11
|
sleep?: (milliseconds: number, signal: AbortSignal) => Promise<void>;
|
|
12
12
|
};
|
|
13
13
|
export declare const TEAM_CLI_ACTIONS: readonly ['connect', 'lead list', 'lead create', 'lead update', 'lead member', 'lead transfer', 'conversation list', 'conversation create', 'conversation show', 'conversation member', 'conversation edit', 'conversation withdraw', 'conversation read', 'conversation follow', 'conversation stop'];
|
|
14
|
+
export declare class UsageError extends Error {
|
|
15
|
+
}
|
|
16
|
+
type Profile = {
|
|
17
|
+
origin: string;
|
|
18
|
+
account: string;
|
|
19
|
+
token: string;
|
|
20
|
+
};
|
|
21
|
+
/** An API token (so_…) is sent bare; an older saved sign-in keeps its account:secret form. */
|
|
22
|
+
export declare const bearerOf: (profile: Profile) => string;
|
|
23
|
+
/** The saved central profile remote commands use: the named one (or the active one), and whether it holds an API token.
|
|
24
|
+
* null when nothing is saved. Reads only the private profile file — never a local login or the database. */
|
|
25
|
+
export declare function centralProfile(options: TeamCliOptions, name?: string): {
|
|
26
|
+
name: string;
|
|
27
|
+
origin: string;
|
|
28
|
+
account: string;
|
|
29
|
+
token: string;
|
|
30
|
+
apiToken: boolean;
|
|
31
|
+
} | null;
|
|
14
32
|
/** null means preserve the existing local command path. Nothing here opens a database. */
|
|
15
33
|
export declare function maybeRunTeamCommand(argv: readonly string[], write: (line: string) => void, options?: TeamCliOptions): Promise<number | null>;
|
|
34
|
+
export {};
|