@koalaauto/agentchannel 3.10.0 → 3.12.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/README.md +5 -1
- package/dist/src/adapters/claude.d.ts +19 -0
- package/dist/src/adapters/claude.js +52 -1
- package/dist/src/adapters/codex.d.ts +11 -0
- package/dist/src/adapters/codex.js +34 -2
- package/dist/src/adapters/fake.d.ts +10 -0
- package/dist/src/adapters/fake.js +9 -0
- package/dist/src/adapters/paths.d.ts +9 -1
- package/dist/src/adapters/paths.js +28 -3
- package/dist/src/adapters/types.d.ts +3 -2
- package/dist/src/args.d.ts +1 -1
- package/dist/src/args.js +18 -9
- package/dist/src/cli.js +81 -17
- package/dist/src/client.d.ts +48 -4
- package/dist/src/client.js +41 -5
- package/dist/src/desknotify.d.ts +2 -2
- package/dist/src/desknotify.js +2 -2
- package/dist/src/doctor.d.ts +4 -0
- package/dist/src/doctor.js +12 -0
- package/dist/src/envcheck.d.ts +22 -0
- package/dist/src/envcheck.js +98 -0
- package/dist/src/mcp.d.ts +1 -1
- package/dist/src/mcp.js +24 -12
- package/dist/src/names.d.ts +6 -1
- package/dist/src/names.js +15 -2
- package/dist/src/onboard.d.ts +11 -2
- package/dist/src/onboard.js +54 -2
- package/dist/src/prompts.d.ts +3 -3
- package/dist/src/prompts.js +4 -4
- package/dist/src/redact.d.ts +5 -0
- package/dist/src/redact.js +41 -0
- package/dist/src/runner/runner.d.ts +32 -1
- package/dist/src/runner/runner.js +214 -20
- package/dist/src/sandboxcheck.d.ts +97 -0
- package/dist/src/sandboxcheck.js +236 -0
- package/dist/src/service.d.ts +9 -3
- package/dist/src/service.js +39 -1
- package/dist/src/trace.d.ts +51 -0
- package/dist/src/trace.js +137 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { execFileSync, execSync, spawn } from 'node:child_process';
|
|
3
3
|
import { randomBytes } from 'node:crypto';
|
|
4
4
|
import { chmodSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { HELP, parseCli, UsageError } from './args.js';
|
|
8
8
|
import { channelRef, Client, clientFromConfig, HubError, isZh, parseLinkRepo } from './client.js';
|
|
9
|
-
import { closest, decisionTo, explainToUser, matchChannel, mentionNames, unknownMentions } from './names.js';
|
|
9
|
+
import { channelTails, closest, decisionTo, explainToUser, matchChannel, mentionNames, unknownMentions } from './names.js';
|
|
10
10
|
import { refOf, untrustedBlock as wrapUntrusted } from './prompts.js';
|
|
11
11
|
import { achHome, loadConfig, readFileConfig, resolveClaimDelay, saveAdminToken, saveAgentKey, saveCfSecret, writeFileConfig } from './config.js';
|
|
12
12
|
import { achCommand, agentInvitePrompt, decodeInvite, encodeInvite, JOIN_HINT, joinLine, mcpCommands, hubTransportOk, shadowHint } from './invite.js';
|
|
@@ -415,7 +415,7 @@ async function dispatch(p, io) {
|
|
|
415
415
|
: `joined ${inv.hub} as agent ${me.name}${me.owner_name ? ` (owner ${me.owner_name})` : ''}; key stored in the ${where === 'keychain' ? 'OS keychain' : 'agent-key file (0600)'}.`);
|
|
416
416
|
const { runSetup, doneLine } = await import('./onboard.js');
|
|
417
417
|
const { defaultExec } = await import('./doctor.js');
|
|
418
|
-
const r = await runSetup({ env, out: io.out, err: io.err, exec: io.exec ?? defaultExec, ask: io.ask, cwd: io.cwd ?? process.cwd() }, { yes: !!v.yes, service: !!v.service, runtime: typeof me.runtime === 'string' ? me.runtime : undefined, zh });
|
|
418
|
+
const r = await runSetup({ env, out: io.out, err: io.err, exec: io.exec ?? defaultExec, ask: io.ask, cwd: io.cwd ?? process.cwd(), ...(io.ask ? { runTty: (line) => runInTerminal(line, env) } : {}) }, { yes: !!v.yes, service: !!v.service, runtime: typeof me.runtime === 'string' ? me.runtime : undefined, zh });
|
|
419
419
|
out(r.failed.length ? (zh ? `部分步骤失败(${r.failed.join(', ')}):运行 ach doctor 查看怎么修。` : `Some steps failed (${r.failed.join(', ')}): run ach doctor to see how to fix them.`) : r.tools.length ? doneLine(r.tools, zh) : zh ? '密钥已保存,但还没有接入任何工具(见上面)。' : 'The key is stored, but no tool is connected yet (see above).');
|
|
420
420
|
return r.failed.length ? 1 : 0;
|
|
421
421
|
}
|
|
@@ -489,6 +489,7 @@ async function dispatch(p, io) {
|
|
|
489
489
|
handoff,
|
|
490
490
|
...(str(v['key-question']) ? { key_question: str(v['key-question']) } : {}),
|
|
491
491
|
...(Array.isArray(v.depends) && v.depends.length ? { depends_on: v.depends } : {}),
|
|
492
|
+
...(v.review ? { review: true } : {}),
|
|
492
493
|
...(links ? { links } : {}),
|
|
493
494
|
...(str(v.channel) ? { channel: channelRef(str(v.channel)) } : {}),
|
|
494
495
|
}, files());
|
|
@@ -643,7 +644,7 @@ async function dispatch(p, io) {
|
|
|
643
644
|
out('no channels');
|
|
644
645
|
else
|
|
645
646
|
for (const c of list)
|
|
646
|
-
out(`#${c.name}${c.archived ? ' [archived]' : ''} ${c.post_count ?? 0} posts${c.last_post_at ? `, last ${c.last_post_at}` : ''}${c.description ? ` ${c.description}` : ''}`);
|
|
647
|
+
out(`#${c.name}${c.archived ? ' [archived]' : ''}${c.access && c.access !== 'team' ? ` [${c.access}]` : ''} ${c.post_count ?? 0} posts${c.last_post_at ? `, last ${c.last_post_at}` : ''}${c.description ? ` ${c.description}` : ''}`);
|
|
647
648
|
return 0;
|
|
648
649
|
}
|
|
649
650
|
case 'channel': {
|
|
@@ -683,6 +684,9 @@ async function dispatch(p, io) {
|
|
|
683
684
|
const chans = await c.channels().catch(() => []);
|
|
684
685
|
const hit = matchChannel(name, chans);
|
|
685
686
|
if (!hit && chans.length) {
|
|
687
|
+
const many = channelTails(name, chans);
|
|
688
|
+
if (many.length > 1)
|
|
689
|
+
throw new UsageError(`post: "${channelRef(name)}" is more than one channel (${many.map((x) => '#' + x.name).join(', ')}): write the full path.`);
|
|
686
690
|
const near = closest(channelRef(name), chans.flatMap((x) => [x.name, String(x.title ?? '')]).filter(Boolean));
|
|
687
691
|
const nearName = near && (chans.find((x) => x.name === near || x.title === near)?.name ?? near);
|
|
688
692
|
throw new UsageError(`post: no channel "${channelRef(name)}" you can post in${nearName ? ` (did you mean #${nearName}?)` : ''}. List them with \`ach channels\`.`);
|
|
@@ -853,23 +857,45 @@ async function dispatch(p, io) {
|
|
|
853
857
|
const runId = str(v['run-id']) ?? (env.ACH_RUN_TOKEN ? undefined : await implicitRunId(c, id, true));
|
|
854
858
|
// A spawned worker says which handoff it worked from; the hub refuses an outdated one.
|
|
855
859
|
const shown = Number(env.ACH_HANDOFF_VERSION) || undefined;
|
|
856
|
-
const r = await c.complete(id, result, runId, att, shown);
|
|
860
|
+
const r = await c.complete(id, result, runId, att, shown, str(v['owner-action']));
|
|
857
861
|
out(v.json ? r : 'completed');
|
|
858
862
|
return 0;
|
|
859
863
|
}
|
|
864
|
+
case 'accept': {
|
|
865
|
+
// the sender's side: a task sent with --review waits for this (or request-changes) when its worker is done
|
|
866
|
+
// `ach accept TASK-12 [note]` or `ach accept --task TASK-12 [note]`
|
|
867
|
+
const id = str(v.task) ?? p.positionals[0];
|
|
868
|
+
if (!id)
|
|
869
|
+
throw new UsageError('accept: <task> is required (TASK-<n>)');
|
|
870
|
+
const note = (str(v.task) ? p.positionals : p.positionals.slice(1)).join(' ').trim();
|
|
871
|
+
const r = await client().acceptTask(id, note || undefined);
|
|
872
|
+
out(v.json ? r : `accepted ${id}`);
|
|
873
|
+
return 0;
|
|
874
|
+
}
|
|
875
|
+
case 'request-changes': {
|
|
876
|
+
const id = str(v.task) ?? p.positionals[0];
|
|
877
|
+
if (!id)
|
|
878
|
+
throw new UsageError('request-changes: <task> is required (TASK-<n>)');
|
|
879
|
+
const text = (str(v.task) ? p.positionals : p.positionals.slice(1)).join(' ').trim();
|
|
880
|
+
if (!text)
|
|
881
|
+
throw new UsageError('request-changes: <task> "<what to change>" is required');
|
|
882
|
+
const r = await client().requestChanges(id, text);
|
|
883
|
+
out(v.json ? r : `sent ${id} back to its worker`);
|
|
884
|
+
return 0;
|
|
885
|
+
}
|
|
860
886
|
case 'fail': {
|
|
861
887
|
const c = client();
|
|
862
888
|
const id = taskIdOf(p, io);
|
|
863
889
|
const reason = text(p, '<reason>');
|
|
864
890
|
const runId = str(v['run-id']) ?? (env.ACH_RUN_TOKEN ? undefined : await implicitRunId(c, id, false));
|
|
865
|
-
const r = await c.fail(id, reason, runId);
|
|
891
|
+
const r = await c.fail(id, reason, runId, str(v['owner-action']));
|
|
866
892
|
out(v.json ? r : 'failed');
|
|
867
893
|
return 0;
|
|
868
894
|
}
|
|
869
895
|
case 'claim': {
|
|
870
896
|
const id = p.positionals[0] ?? str(v.task);
|
|
871
897
|
if (!id)
|
|
872
|
-
throw new UsageError('claim: <task> is required (id or
|
|
898
|
+
throw new UsageError('claim: <task> is required (id or TASK-<n>)');
|
|
873
899
|
const c = client();
|
|
874
900
|
const me = await c.me();
|
|
875
901
|
const b = await c.claim(id, newInteractiveRunId(), 'interactive');
|
|
@@ -894,7 +920,7 @@ async function dispatch(p, io) {
|
|
|
894
920
|
case 'remember': {
|
|
895
921
|
const source = str(v.source);
|
|
896
922
|
if (!source)
|
|
897
|
-
throw new UsageError('remember: --source <link> is required (a hub permalink like #/t/
|
|
923
|
+
throw new UsageError('remember: --source <link> is required (a hub permalink like #/t/TASK-12 or a GitHub URL)');
|
|
898
924
|
const r = await client().remember(text(p, '"<fact or lesson>"'), source, { channel: str(v.channel), topic: str(v.topic), toTeam: !!v['to-team'] });
|
|
899
925
|
if (v.json)
|
|
900
926
|
out(r);
|
|
@@ -1318,12 +1344,12 @@ async function dispatch(p, io) {
|
|
|
1318
1344
|
out(list);
|
|
1319
1345
|
else
|
|
1320
1346
|
for (const c of list)
|
|
1321
|
-
out(`#${c.name}${c.title && c.title !== c.name ? ` ${c.title}` : ''}${c.archived ? ' [archived]' : ''}${c.visibility === 'private' ? ` [private, ${c.member_count ?? 0} members]` : ''} ${c.post_count ?? 0} posts`);
|
|
1347
|
+
out(`#${c.name}${c.title && c.title !== c.name ? ` ${c.title}` : ''}${c.archived ? ' [archived]' : ''}${c.visibility === 'private' ? ` [${c.access && c.access !== 'private' ? c.access : 'private'}, ${c.member_count ?? 0} members]` : ''} ${c.post_count ?? 0} posts`);
|
|
1322
1348
|
return 0;
|
|
1323
1349
|
}
|
|
1324
1350
|
if (sub === 'create') {
|
|
1325
1351
|
const title = str(v.title) ?? rest.join(' ').trim();
|
|
1326
|
-
const r = await a.request('POST', '/api/ach/channels', { ...(str(v.name) ? { name: str(v.name) } : {}), ...(title ? { title } : {}), ...(str(v.description) ? { description: str(v.description) } : {}) });
|
|
1352
|
+
const r = await a.request('POST', '/api/ach/channels', { ...(str(v.name) ? { name: str(v.name) } : {}), ...(str(v.folder) ? { folder: str(v.folder) } : {}), ...(title ? { title } : {}), ...(str(v.description) ? { description: str(v.description) } : {}) });
|
|
1327
1353
|
out(v.json ? r : `created #${r.name}`);
|
|
1328
1354
|
return 0;
|
|
1329
1355
|
}
|
|
@@ -1338,10 +1364,18 @@ async function dispatch(p, io) {
|
|
|
1338
1364
|
// private: only members (and admins) see the channel, its posts, wiki pages and files
|
|
1339
1365
|
if (!rest[0])
|
|
1340
1366
|
throw new UsageError(`admin channels ${sub}: <#name> is required`);
|
|
1341
|
-
const r = await a.request('PATCH', `/api/ach/channels/${encodeURIComponent(channelRef(rest[0]))}`, { visibility: sub });
|
|
1367
|
+
const r = await a.request('PATCH', `/api/ach/channels/${encodeURIComponent(channelRef(rest[0]))}`, { visibility: sub, ...(v.cascade ? { cascade: true } : {}) });
|
|
1342
1368
|
out(v.json ? r : `#${r.name}: ${r.visibility}`);
|
|
1343
1369
|
return 0;
|
|
1344
1370
|
}
|
|
1371
|
+
if (sub === 'move' || sub === 'rename') {
|
|
1372
|
+
// a new path: what is under it goes along, and the old path keeps finding it
|
|
1373
|
+
if (!rest[0] || !rest[1])
|
|
1374
|
+
throw new UsageError(`admin channels ${sub}: <#old-path> <#new-path> is required (e.g. onair-mmgr reela/mmgr)`);
|
|
1375
|
+
const r = await a.request('PATCH', `/api/ach/channels/${encodeURIComponent(channelRef(rest[0]))}`, { name: channelRef(rest[1]) });
|
|
1376
|
+
out(v.json ? r : `#${r.name} (the old path #${channelRef(rest[0])} still finds it)`);
|
|
1377
|
+
return 0;
|
|
1378
|
+
}
|
|
1345
1379
|
if (sub === 'members') {
|
|
1346
1380
|
if (!rest[0])
|
|
1347
1381
|
throw new UsageError('admin channels members: <#name> [--add a,b] [--remove c] is required');
|
|
@@ -1356,7 +1390,7 @@ async function dispatch(p, io) {
|
|
|
1356
1390
|
out(v.json ? r : `#${r?.name}: ${r?.visibility ?? 'team'}, ${r?.member_count ?? 0} member(s)`);
|
|
1357
1391
|
return 0;
|
|
1358
1392
|
}
|
|
1359
|
-
throw new UsageError('admin channels: list | create [--name
|
|
1393
|
+
throw new UsageError('admin channels: list | create [--name path] [--folder f] [--title t] | move <#old> <#new-path> | archive <#name> | unarchive <#name> | private <#name> [--cascade] | team <#name> | members <#name> [--add a,b] [--remove c]');
|
|
1360
1394
|
}
|
|
1361
1395
|
case 'admin embeds': {
|
|
1362
1396
|
const a = admin();
|
|
@@ -1397,6 +1431,20 @@ async function dispatch(p, io) {
|
|
|
1397
1431
|
}
|
|
1398
1432
|
return 0;
|
|
1399
1433
|
}
|
|
1434
|
+
case 'admin memory-maintainers': {
|
|
1435
|
+
const a = admin();
|
|
1436
|
+
const names = p.positionals.flatMap(splitList);
|
|
1437
|
+
const r = (names.length ? await a.request('PUT', '/api/ach/admin/memory/settings', { maintainers: { agents: names } }) : await a.request('GET', '/api/ach/admin/memory/settings'));
|
|
1438
|
+
const list = (r.maintainers?.agents ?? []);
|
|
1439
|
+
out(v.json ? r : list.length ? `Memory maintainers: ${list.join(', ')}` : 'no Memory maintainer is set: the admin agent with no setup problem that was seen most recently is used (name one to choose)');
|
|
1440
|
+
return 0;
|
|
1441
|
+
}
|
|
1442
|
+
case 'admin memory-upkeep': {
|
|
1443
|
+
const name = p.positionals[0];
|
|
1444
|
+
const r = (await admin().request('POST', '/api/ach/admin/memory/overview', name ? { channel: channelRef(name) } : {}));
|
|
1445
|
+
out(v.json ? r : r.upkeep_task ? `queued ${r.upkeep_task.ref}${r.upkeep_task.url ? `: ${r.upkeep_task.url}` : ''}` : 'nothing waiting for upkeep');
|
|
1446
|
+
return 0;
|
|
1447
|
+
}
|
|
1400
1448
|
case 'admin retention': {
|
|
1401
1449
|
const name = p.positionals[0];
|
|
1402
1450
|
if (!name)
|
|
@@ -1485,6 +1533,14 @@ function linkOf(env, t) {
|
|
|
1485
1533
|
function splitList(v) {
|
|
1486
1534
|
return v.split(',').map((x) => x.trim()).filter(Boolean);
|
|
1487
1535
|
}
|
|
1536
|
+
/** A shell line with this terminal's stdin/stdout/stderr (sudo asks for its password there); its exit code. */
|
|
1537
|
+
function runInTerminal(line, env) {
|
|
1538
|
+
return new Promise((res) => {
|
|
1539
|
+
const c = spawn('/bin/sh', ['-c', line], { env, stdio: 'inherit' });
|
|
1540
|
+
c.on('error', () => res(127));
|
|
1541
|
+
c.on('exit', (code) => res(code ?? 1));
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1488
1544
|
function shq(s) {
|
|
1489
1545
|
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1490
1546
|
}
|
|
@@ -1511,7 +1567,7 @@ async function runCommand(p, io) {
|
|
|
1511
1567
|
const cfg = loadConfig(env);
|
|
1512
1568
|
if (!cfg.key)
|
|
1513
1569
|
throw new UsageError('run: no agent key. Run `ach login` or set ACH_KEY.');
|
|
1514
|
-
const c = clientFromConfig(cfg, { fetch: io.fetch });
|
|
1570
|
+
const c = clientFromConfig(cfg, { fetch: io.fetch, role: 'runner' });
|
|
1515
1571
|
const home = achHome(env);
|
|
1516
1572
|
const dir = resolve(io.cwd ?? process.cwd(), str(v.dir) ?? '.');
|
|
1517
1573
|
const { GitWorkspace, parseRepoFlags } = await import('./runner/workspace.js');
|
|
@@ -1568,6 +1624,7 @@ async function runCommand(p, io) {
|
|
|
1568
1624
|
claimDelayMs: claimDelay * 1000,
|
|
1569
1625
|
cfId: cfg.cfId,
|
|
1570
1626
|
cfSecret: cfg.cfSecret,
|
|
1627
|
+
secrets: [cfg.key ?? ''].filter(Boolean),
|
|
1571
1628
|
publicUrl: env.ACH_PUBLIC_URL || undefined,
|
|
1572
1629
|
onClientTooOld: () => void updater?.nudge(),
|
|
1573
1630
|
passEnv: (Array.isArray(v['pass-env']) ? v['pass-env'] : []).flatMap(splitList),
|
|
@@ -1576,6 +1633,10 @@ async function runCommand(p, io) {
|
|
|
1576
1633
|
return shim ? { ...env, PATH: `${shim}${delimiter}${env.PATH ?? ''}` } : env;
|
|
1577
1634
|
})(),
|
|
1578
1635
|
log: io.err,
|
|
1636
|
+
sandboxCheck: async () => {
|
|
1637
|
+
const { probeEnvironment } = await import('./envcheck.js');
|
|
1638
|
+
return probeEnvironment(runtime === 'codex' ? 'codex' : 'claude', { env });
|
|
1639
|
+
},
|
|
1579
1640
|
});
|
|
1580
1641
|
io.err(`ach run: agent ${me.name}, runtime ${runtime}, perm min(${me.perm_level}, ${perm}), dir ${dir}` +
|
|
1581
1642
|
(Object.keys(repos).length ? `, repos ${Object.entries(repos).map(([k, x]) => `${k}=${x}`).join(' ')}` : '') +
|
|
@@ -1666,8 +1727,8 @@ async function installService(p, io, o) {
|
|
|
1666
1727
|
const { isBundledBin, npmSpec, whichAll } = await import('./invite.js');
|
|
1667
1728
|
const env = io.env;
|
|
1668
1729
|
const platform = (env.ACH_SERVICE_PLATFORM || process.platform);
|
|
1669
|
-
if (platform !== 'darwin' && platform !== 'linux')
|
|
1670
|
-
throw new UsageError(`run --install-service: unsupported platform ${platform} (macOS
|
|
1730
|
+
if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win32')
|
|
1731
|
+
throw new UsageError(`run --install-service: unsupported platform ${platform} (macOS, Linux and Windows only)`);
|
|
1671
1732
|
const v = p.values;
|
|
1672
1733
|
const args = ['--dir', o.dir];
|
|
1673
1734
|
for (const [name, path] of Object.entries(o.repos))
|
|
@@ -1704,14 +1765,17 @@ async function installService(p, io, o) {
|
|
|
1704
1765
|
args,
|
|
1705
1766
|
env: svcEnv,
|
|
1706
1767
|
logFile: join(o.home, 'logs', 'runner.log'),
|
|
1707
|
-
}, env.ACH_SERVICE_DIR || undefined);
|
|
1768
|
+
}, env.ACH_SERVICE_DIR || undefined, env);
|
|
1708
1769
|
writeServiceFile(f);
|
|
1709
1770
|
const load = !env.ACH_SERVICE_DIR;
|
|
1710
1771
|
let failed = '';
|
|
1711
1772
|
if (load) {
|
|
1712
1773
|
for (const c of f.start) {
|
|
1713
1774
|
try {
|
|
1714
|
-
|
|
1775
|
+
if (platform === 'win32')
|
|
1776
|
+
execSync(c, { env, stdio: ['ignore', 'ignore', 'pipe'] }); // cmd.exe
|
|
1777
|
+
else
|
|
1778
|
+
execFileSync('/bin/sh', ['-c', c], { env, stdio: ['ignore', 'ignore', 'pipe'] });
|
|
1715
1779
|
}
|
|
1716
1780
|
catch (e) {
|
|
1717
1781
|
failed = `${c}: ${String(e?.stderr ?? e?.message ?? e).trim().split('\n')[0]}`;
|
package/dist/src/client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Config } from './config.js';
|
|
2
2
|
import { type Attachment, type StorageConfig, type TaskFile } from './attachments.js';
|
|
3
|
-
export type TaskStatus = 'submitted' | 'working' | 'input-required' | 'completed' | 'failed' | 'canceled';
|
|
3
|
+
export type TaskStatus = 'submitted' | 'working' | 'input-required' | 'awaiting_acceptance' | 'completed' | 'failed' | 'canceled';
|
|
4
4
|
export declare const TERMINAL: readonly TaskStatus[];
|
|
5
5
|
export declare function isTerminal(s: string | undefined): boolean;
|
|
6
6
|
export type MessageKind = 'question' | 'answer' | 'decision_request' | 'decision' | 'progress' | 'result' | 'note';
|
|
@@ -43,7 +43,7 @@ export interface Task {
|
|
|
43
43
|
result?: string;
|
|
44
44
|
created?: string;
|
|
45
45
|
updated?: string;
|
|
46
|
-
/** §19: human-friendly number ("
|
|
46
|
+
/** §19: human-friendly number ("TASK-128" = ref) and permalink. */
|
|
47
47
|
seq?: number;
|
|
48
48
|
ref?: string;
|
|
49
49
|
url?: string;
|
|
@@ -142,6 +142,8 @@ export interface ClaimResult extends TaskBundle {
|
|
|
142
142
|
export type ClaimMode = 'runner' | 'interactive';
|
|
143
143
|
export interface Inbox {
|
|
144
144
|
tasks_to_me: Task[];
|
|
145
|
+
/** Tasks that said done and wait for acceptance, which this agent may accept or send back (accept_task / request_changes). */
|
|
146
|
+
tasks_to_review?: Task[];
|
|
145
147
|
questions_to_me: Message[];
|
|
146
148
|
answers_to_me: Message[];
|
|
147
149
|
decisions_for_me: Message[];
|
|
@@ -175,7 +177,15 @@ export interface Post {
|
|
|
175
177
|
}
|
|
176
178
|
export interface Channel {
|
|
177
179
|
id: string;
|
|
180
|
+
/** The path: `reela/onair` (up to 3 levels). */
|
|
178
181
|
name: string;
|
|
182
|
+
/** The folder it sits in ('' at the top). */
|
|
183
|
+
parent?: string;
|
|
184
|
+
depth?: number;
|
|
185
|
+
/** team | private | inherited (follows its parent's people) | narrowed (a subset of them). */
|
|
186
|
+
access?: 'team' | 'private' | 'inherited' | 'narrowed';
|
|
187
|
+
/** Paths it had before a rename or move; they still find it. */
|
|
188
|
+
former_names?: string[];
|
|
179
189
|
description?: string;
|
|
180
190
|
archived?: boolean;
|
|
181
191
|
post_count?: number;
|
|
@@ -228,6 +238,8 @@ export interface SendTaskInput {
|
|
|
228
238
|
key_question?: string;
|
|
229
239
|
depends_on?: string[];
|
|
230
240
|
links?: Record<string, unknown>;
|
|
241
|
+
/** The task waits for acceptance when its worker is done (status awaiting_acceptance) until the sender's side accepts it or sends it back. */
|
|
242
|
+
review?: boolean;
|
|
231
243
|
/** §16: channel name; default = the channel named like links.repo, else #general. */
|
|
232
244
|
channel?: string;
|
|
233
245
|
}
|
|
@@ -288,6 +300,8 @@ export interface ClientOptions {
|
|
|
288
300
|
cfId?: string;
|
|
289
301
|
cfSecret?: string;
|
|
290
302
|
fetch?: FetchLike;
|
|
303
|
+
/** `runner`: this is `ach run` polling, not a person's session or an MCP server (the hub notes which agents have a runner). */
|
|
304
|
+
role?: 'runner';
|
|
291
305
|
}
|
|
292
306
|
/** Round 2: human-facing CLI text in Chinese when LANG/LC_ALL starts with zh. */
|
|
293
307
|
export declare function isZh(env: NodeJS.ProcessEnv): boolean;
|
|
@@ -295,6 +309,7 @@ export declare function isZh(env: NodeJS.ProcessEnv): boolean;
|
|
|
295
309
|
export declare function clientFromConfig(cfg: Config, opts?: {
|
|
296
310
|
admin?: boolean;
|
|
297
311
|
fetch?: FetchLike;
|
|
312
|
+
role?: 'runner';
|
|
298
313
|
}): Client;
|
|
299
314
|
/** Accept either `{task, handoff, messages, history}` or a bare task record. */
|
|
300
315
|
export declare function normalizeBundle(raw: any): TaskBundle;
|
|
@@ -305,6 +320,7 @@ export declare class Client {
|
|
|
305
320
|
private cfSecret?;
|
|
306
321
|
private fetchFn;
|
|
307
322
|
constructor(o: ClientOptions);
|
|
323
|
+
private role?;
|
|
308
324
|
private ownFetch;
|
|
309
325
|
/**
|
|
310
326
|
* The hub's change stream (GET /api/ach/events, a WebSocket that says "something changed", ids only). The token
|
|
@@ -338,6 +354,10 @@ export declare class Client {
|
|
|
338
354
|
me(): Promise<Agent>;
|
|
339
355
|
agents(): Promise<Agent[]>;
|
|
340
356
|
sendTask(input: SendTaskInput, files?: string[]): Promise<Task>;
|
|
357
|
+
/** The sender's side accepts a task that waits for acceptance: it is completed. */
|
|
358
|
+
acceptTask(taskId: string, note?: string): Promise<any>;
|
|
359
|
+
/** The sender's side sends a finished, failed or waiting task back with what to change; the worker redoes it. */
|
|
360
|
+
requestChanges(taskId: string, body: string): Promise<any>;
|
|
341
361
|
/** Propose a recurring task (runs after the owner approves). */
|
|
342
362
|
proposeSchedule(input: Record<string, unknown>): Promise<any>;
|
|
343
363
|
schedules(): Promise<any[]>;
|
|
@@ -356,8 +376,9 @@ export declare class Client {
|
|
|
356
376
|
}>;
|
|
357
377
|
postMessage(taskId: string, m: PostMessageInput, files?: string[]): Promise<Message>;
|
|
358
378
|
/** With an agent key the hub requires the task's current run_id (C3); run tokens carry their own. */
|
|
359
|
-
|
|
360
|
-
|
|
379
|
+
/** `owner_action`: a step only the owner of the executing agent can do; it reaches them as an Inbox item. */
|
|
380
|
+
complete(taskId: string, result: string, run_id?: string, files?: string[], handoff_version?: number, owner_action?: string): Promise<any>;
|
|
381
|
+
fail(taskId: string, reason: string, run_id?: string, owner_action?: string): Promise<any>;
|
|
361
382
|
/** §23: a grantee agent answers the part of a request its grant covers. */
|
|
362
383
|
grantDecide(taskId: string, message_id: string, option: number, grant: string): Promise<any>;
|
|
363
384
|
createGrant(input: {
|
|
@@ -379,6 +400,29 @@ export declare class Client {
|
|
|
379
400
|
/** The total size of a work prompt (Memory trial metrics). */
|
|
380
401
|
promptSize(taskId: string, run_id: string, chars: number, parts?: Record<string, number>): Promise<any>;
|
|
381
402
|
usage(taskId: string, run_id: string, cost_usd: number, estimated: boolean): Promise<any>;
|
|
403
|
+
/**
|
|
404
|
+
* design §27: what keeps this agent's runner from taking tasks on its machine (null: nothing). The hub shows it as
|
|
405
|
+
* "needs setup" with the fix.
|
|
406
|
+
*/
|
|
407
|
+
/**
|
|
408
|
+
* The execution trace of one run (or one stretch of it), already masked and cut down: steps with thinking summaries, commands,
|
|
409
|
+
* exit codes and output excerpts. Idempotent by `traceId`; a second call updates the fields it carries (the commit, say).
|
|
410
|
+
*/
|
|
411
|
+
putTrace(taskId: string, traceId: string, body: Record<string, unknown>): Promise<any>;
|
|
412
|
+
/** The traces of a task, oldest first. */
|
|
413
|
+
traces(taskId: string): Promise<any>;
|
|
414
|
+
/** The worker cannot run on its machine (not logged in, sandbox broken): the task goes back to submitted, not failed. */
|
|
415
|
+
release(taskId: string, reason: string, run_id?: string): Promise<any>;
|
|
416
|
+
reportSetup(sandbox: {
|
|
417
|
+
tool: string;
|
|
418
|
+
problem: string;
|
|
419
|
+
detail?: string;
|
|
420
|
+
fix?: {
|
|
421
|
+
en: string;
|
|
422
|
+
zh: string;
|
|
423
|
+
commands: string[];
|
|
424
|
+
};
|
|
425
|
+
} | null): Promise<any>;
|
|
382
426
|
/** 409 when the run lost the task. Carries a fresh `run_token` when the old one is near expiry (C5). */
|
|
383
427
|
heartbeat(taskId: string, run_id: string): Promise<{
|
|
384
428
|
ok: boolean;
|
package/dist/src/client.js
CHANGED
|
@@ -44,7 +44,7 @@ export function clientFromConfig(cfg, opts = {}) {
|
|
|
44
44
|
: 'No hub configured: run the `ach join ach2....` line from the panel or your admin (or set ACH_HUB).');
|
|
45
45
|
}
|
|
46
46
|
const token = opts.admin ? cfg.adminToken : cfg.runToken || cfg.key;
|
|
47
|
-
return new Client({ hub: cfg.hub, token, cfId: cfg.cfId, cfSecret: cfg.cfSecret, fetch: opts.fetch });
|
|
47
|
+
return new Client({ hub: cfg.hub, token, cfId: cfg.cfId, cfSecret: cfg.cfSecret, fetch: opts.fetch, role: opts.role });
|
|
48
48
|
}
|
|
49
49
|
function arr(v) {
|
|
50
50
|
return Array.isArray(v) ? v : [];
|
|
@@ -74,7 +74,9 @@ export class Client {
|
|
|
74
74
|
this.cfSecret = o.cfSecret;
|
|
75
75
|
this.fetchFn = o.fetch ?? ((u, i) => fetch(u, i));
|
|
76
76
|
this.ownFetch = !!o.fetch;
|
|
77
|
+
this.role = o.role;
|
|
77
78
|
}
|
|
79
|
+
role;
|
|
78
80
|
ownFetch;
|
|
79
81
|
/**
|
|
80
82
|
* The hub's change stream (GET /api/ach/events, a WebSocket that says "something changed", ids only). The token
|
|
@@ -95,6 +97,8 @@ export class Client {
|
|
|
95
97
|
}
|
|
96
98
|
headers(json = true) {
|
|
97
99
|
const h = { ...CLIENT_HEADERS, ...(json ? { 'content-type': 'application/json', accept: 'application/json' } : { accept: 'application/json' }) };
|
|
100
|
+
if (this.role)
|
|
101
|
+
h['x-ach-role'] = this.role;
|
|
98
102
|
if (this.token) {
|
|
99
103
|
// Agent keys and run tokens use Bearer; a person's session token is sent as it is.
|
|
100
104
|
h.authorization = /^ach_(sk|rt)_/.test(this.token) ? `Bearer ${this.token}` : this.token;
|
|
@@ -225,6 +229,14 @@ export class Client {
|
|
|
225
229
|
const r = await this.ach('POST', '/tasks', input, files);
|
|
226
230
|
return r?.task ?? r;
|
|
227
231
|
}
|
|
232
|
+
/** The sender's side accepts a task that waits for acceptance: it is completed. */
|
|
233
|
+
async acceptTask(taskId, note) {
|
|
234
|
+
return this.ach('POST', `/tasks/${enc(taskId)}/accept`, note ? { note } : {});
|
|
235
|
+
}
|
|
236
|
+
/** The sender's side sends a finished, failed or waiting task back with what to change; the worker redoes it. */
|
|
237
|
+
async requestChanges(taskId, body) {
|
|
238
|
+
return this.ach('POST', `/tasks/${enc(taskId)}/request-changes`, { body });
|
|
239
|
+
}
|
|
228
240
|
/** Propose a recurring task (runs after the owner approves). */
|
|
229
241
|
async proposeSchedule(input) {
|
|
230
242
|
return this.ach('POST', '/schedules', input);
|
|
@@ -268,11 +280,12 @@ export class Client {
|
|
|
268
280
|
return r?.message ?? r;
|
|
269
281
|
}
|
|
270
282
|
/** With an agent key the hub requires the task's current run_id (C3); run tokens carry their own. */
|
|
271
|
-
|
|
272
|
-
|
|
283
|
+
/** `owner_action`: a step only the owner of the executing agent can do; it reaches them as an Inbox item. */
|
|
284
|
+
async complete(taskId, result, run_id, files, handoff_version, owner_action) {
|
|
285
|
+
return this.ach('POST', `/tasks/${enc(taskId)}/complete`, { result, ...(run_id ? { run_id } : {}), ...(handoff_version ? { handoff_version } : {}), ...(owner_action ? { owner_action } : {}) }, files);
|
|
273
286
|
}
|
|
274
|
-
async fail(taskId, reason, run_id) {
|
|
275
|
-
return this.ach('POST', `/tasks/${enc(taskId)}/fail`, { reason, ...(run_id ? { run_id } : {}) });
|
|
287
|
+
async fail(taskId, reason, run_id, owner_action) {
|
|
288
|
+
return this.ach('POST', `/tasks/${enc(taskId)}/fail`, { reason, ...(run_id ? { run_id } : {}), ...(owner_action ? { owner_action } : {}) });
|
|
276
289
|
}
|
|
277
290
|
/** §23: a grantee agent answers the part of a request its grant covers. */
|
|
278
291
|
async grantDecide(taskId, message_id, option, grant) {
|
|
@@ -297,6 +310,28 @@ export class Client {
|
|
|
297
310
|
async usage(taskId, run_id, cost_usd, estimated) {
|
|
298
311
|
return this.ach('POST', `/tasks/${enc(taskId)}/usage`, { run_id, cost_usd, estimated });
|
|
299
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* design §27: what keeps this agent's runner from taking tasks on its machine (null: nothing). The hub shows it as
|
|
315
|
+
* "needs setup" with the fix.
|
|
316
|
+
*/
|
|
317
|
+
/**
|
|
318
|
+
* The execution trace of one run (or one stretch of it), already masked and cut down: steps with thinking summaries, commands,
|
|
319
|
+
* exit codes and output excerpts. Idempotent by `traceId`; a second call updates the fields it carries (the commit, say).
|
|
320
|
+
*/
|
|
321
|
+
async putTrace(taskId, traceId, body) {
|
|
322
|
+
return this.ach('PUT', `/tasks/${enc(taskId)}/traces/${enc(traceId)}`, body);
|
|
323
|
+
}
|
|
324
|
+
/** The traces of a task, oldest first. */
|
|
325
|
+
async traces(taskId) {
|
|
326
|
+
return this.ach('GET', `/tasks/${enc(taskId)}/traces`);
|
|
327
|
+
}
|
|
328
|
+
/** The worker cannot run on its machine (not logged in, sandbox broken): the task goes back to submitted, not failed. */
|
|
329
|
+
async release(taskId, reason, run_id) {
|
|
330
|
+
return this.ach('POST', `/tasks/${enc(taskId)}/release`, { reason, ...(run_id ? { run_id } : {}) });
|
|
331
|
+
}
|
|
332
|
+
async reportSetup(sandbox) {
|
|
333
|
+
return this.ach('PUT', '/me/setup', { sandbox });
|
|
334
|
+
}
|
|
300
335
|
/** 409 when the run lost the task. Carries a fresh `run_token` when the old one is near expiry (C5). */
|
|
301
336
|
async heartbeat(taskId, run_id) {
|
|
302
337
|
return this.ach('POST', `/tasks/${enc(taskId)}/heartbeat`, { run_id });
|
|
@@ -309,6 +344,7 @@ export class Client {
|
|
|
309
344
|
const r = await this.ach('GET', '/inbox');
|
|
310
345
|
return {
|
|
311
346
|
tasks_to_me: arr(r?.tasks_to_me),
|
|
347
|
+
tasks_to_review: arr(r?.tasks_to_review),
|
|
312
348
|
questions_to_me: flatMsgs(r?.questions_to_me),
|
|
313
349
|
answers_to_me: flatMsgs(r?.answers_to_me),
|
|
314
350
|
decisions_for_me: flatMsgs(r?.decisions_for_me),
|
package/dist/src/desknotify.d.ts
CHANGED
|
@@ -77,13 +77,13 @@ export declare function notifyCommand(o: {
|
|
|
77
77
|
} | null;
|
|
78
78
|
/** The options a decision item offers (labels/details from `options`, tokens from `actions` by option index). Pure. */
|
|
79
79
|
export declare function itemOptions(x: NotifyItem): NotifyOption[];
|
|
80
|
-
/** Task ref and message id of a decision item: fields, else its link `t/
|
|
80
|
+
/** Task ref and message id of a decision item: fields, else its link `t/TASK-12/m/<id>`. Pure. */
|
|
81
81
|
export declare function itemTarget(x: NotifyItem): {
|
|
82
82
|
task?: string;
|
|
83
83
|
message?: string;
|
|
84
84
|
};
|
|
85
85
|
/**
|
|
86
|
-
* The panel link with option `i` preselected: `…/#/t/
|
|
86
|
+
* The panel link with option `i` preselected: `…/#/t/TASK-12/m/<id>?pick=<i>`
|
|
87
87
|
* (the query goes inside the hash route, where the panel reads it). Pure.
|
|
88
88
|
*/
|
|
89
89
|
export declare function pickUrl(url: string, i: number): string;
|
package/dist/src/desknotify.js
CHANGED
|
@@ -121,7 +121,7 @@ export function itemOptions(x) {
|
|
|
121
121
|
});
|
|
122
122
|
return out.slice(0, 4);
|
|
123
123
|
}
|
|
124
|
-
/** Task ref and message id of a decision item: fields, else its link `t/
|
|
124
|
+
/** Task ref and message id of a decision item: fields, else its link `t/TASK-12/m/<id>`. Pure. */
|
|
125
125
|
export function itemTarget(x) {
|
|
126
126
|
const m = /(?:^|#\/|\/)t\/([^/?#]+)\/m\/([A-Za-z0-9_-]+)/.exec(String(x.path ?? '') || String(x.url ?? ''));
|
|
127
127
|
return { task: x.task_id || x.task_ref || (m ? decodeURIComponent(m[1]) : undefined), message: x.message_id || (m ? m[2] : undefined) };
|
|
@@ -132,7 +132,7 @@ function isDecision(x) {
|
|
|
132
132
|
return x.decision === true || /decision_request|memory_question/.test(String(x.event ?? '')) || itemOptions(x).length > 0;
|
|
133
133
|
}
|
|
134
134
|
/**
|
|
135
|
-
* The panel link with option `i` preselected: `…/#/t/
|
|
135
|
+
* The panel link with option `i` preselected: `…/#/t/TASK-12/m/<id>?pick=<i>`
|
|
136
136
|
* (the query goes inside the hash route, where the panel reads it). Pure.
|
|
137
137
|
*/
|
|
138
138
|
export function pickUrl(url, i) {
|
package/dist/src/doctor.d.ts
CHANGED
|
@@ -49,6 +49,10 @@ export declare function runDoctor(o: {
|
|
|
49
49
|
fetch?: FetchLike;
|
|
50
50
|
exec?: ExecLike;
|
|
51
51
|
self?: string;
|
|
52
|
+
/** the sandbox probe (tests) */
|
|
53
|
+
sandbox?: (tool: 'claude' | 'codex', o: {
|
|
54
|
+
env: NodeJS.ProcessEnv;
|
|
55
|
+
}) => Promise<import('./sandboxcheck.js').SandboxStatus>;
|
|
52
56
|
}): Promise<DoctorCheck[]>;
|
|
53
57
|
/**
|
|
54
58
|
* Command, args and AGENTCHANNEL_HOME of an MCP entry from `claude mcp get`
|
package/dist/src/doctor.js
CHANGED
|
@@ -247,6 +247,7 @@ export async function runDoctor(o) {
|
|
|
247
247
|
const { serviceFile, serviceLabel } = await import('./service.js');
|
|
248
248
|
const platform = (env.ACH_SERVICE_PLATFORM || process.platform);
|
|
249
249
|
if (platform === 'darwin' || platform === 'linux') {
|
|
250
|
+
// (Windows: the Startup script has no status command; doctor says nothing rather than guess)
|
|
250
251
|
const f = serviceFile({ platform, agentName, command: [], args: [], env: {}, logFile: '' }, env.ACH_SERVICE_DIR || undefined);
|
|
251
252
|
if (!existsSync(f.path))
|
|
252
253
|
add('background', 'info', 'no background runner on this machine (optional: ach run --dir <repo> --install-service --interactive-first)');
|
|
@@ -299,6 +300,17 @@ export async function runDoctor(o) {
|
|
|
299
300
|
if (codexPath)
|
|
300
301
|
await mcpCheck('codex', codexPath);
|
|
301
302
|
}
|
|
303
|
+
// design §27: the agent tool's sandbox, probed for real (every run that answers a question or an @mention is sandboxed)
|
|
304
|
+
const { probeSandbox } = await import('./sandboxcheck.js');
|
|
305
|
+
const tools = runtime === 'codex' ? ['codex'] : runtime === 'claude' ? ['claude'] : [...(claude ? ['claude'] : []), ...(codexPath || bundled ? ['codex'] : [])];
|
|
306
|
+
for (const t of tools) {
|
|
307
|
+
const st = await (o.sandbox ?? probeSandbox)(t, { env });
|
|
308
|
+
const name = `${t} sandbox`;
|
|
309
|
+
if (st.ok)
|
|
310
|
+
add(name, st.unverified ? 'info' : 'ok', st.unverified ? `not checked: ${st.detail ?? 'unknown'}` : 'starts, and stops writes outside the task directory');
|
|
311
|
+
else
|
|
312
|
+
add(name, runtime ? 'fail' : 'warn', `${st.fix?.en ?? st.detail ?? st.problem}${runtime ? ' The background runner takes no tasks until this is fixed.' : ''}`, 'machine', st.fix?.commands.length ? st.fix.commands.join('\n ') : st.fix?.en);
|
|
313
|
+
}
|
|
302
314
|
return out;
|
|
303
315
|
/**
|
|
304
316
|
* The `ach` MCP entry as the runtime itself reports it (`<runtime> mcp get
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ProbeOptions, type Run, type SandboxStatus, type SandboxTool } from './sandboxcheck.js';
|
|
2
|
+
/** The tool authenticates from the environment (an API key, a cloud provider): there is no login to check. */
|
|
3
|
+
export declare function authFromEnv(tool: SandboxTool, env: NodeJS.ProcessEnv): string | undefined;
|
|
4
|
+
/** Is `tool` logged in here? Never throws. A tool that is not installed, or whose status command cannot be read, does not block (unverified). */
|
|
5
|
+
export declare function probeLogin(tool: SandboxTool, o?: {
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
run?: Run;
|
|
8
|
+
claude?: string;
|
|
9
|
+
codex?: string[];
|
|
10
|
+
platform?: NodeJS.Platform;
|
|
11
|
+
}): Promise<SandboxStatus>;
|
|
12
|
+
/** The whole check: login first (cheap, and what a fresh machine lacks), then the sandbox. The first problem is the answer. */
|
|
13
|
+
export declare function probeEnvironment(tool: SandboxTool, o?: ProbeOptions & {
|
|
14
|
+
claude?: string;
|
|
15
|
+
}): Promise<SandboxStatus>;
|
|
16
|
+
/**
|
|
17
|
+
* A run's error as the task's failure reason: the lines that carry the cause, not just the first one. Noise lines are dropped,
|
|
18
|
+
* lines that look like a cause come first (at most 3), then the first remaining line; capped at 600 characters.
|
|
19
|
+
*/
|
|
20
|
+
export declare function summarizeRuntimeError(raw: string): string;
|
|
21
|
+
/** Does a run's error say the tool is not logged in (or its key was refused)? */
|
|
22
|
+
export declare function looksLikeLoginError(s: string): boolean;
|