agents-can-communicate 0.10.3 → 0.11.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 +29 -21
- package/bin/acc-indicator.mjs +5 -0
- package/docs/CAPABILITIES.md +3 -0
- package/docs/CLI.md +34 -7
- package/docs/MCP.md +42 -2
- package/docs/PROTOCOL.md +19 -0
- package/docs/STATUS_INDICATOR.md +128 -0
- package/docs/TROUBLESHOOTING.md +10 -0
- package/docs/UPGRADING.md +16 -0
- package/docs/WHY_ACC.md +14 -8
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +3 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-install.mjs +80 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-render.mjs +8 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +7 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/hooks/indicator.mjs +67 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +16 -5
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +48 -5
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +9 -6
- package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +16 -5
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +4 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/indicator-install.mjs +30 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/indicator.mjs +23 -0
- package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +9 -5
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +4 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/indicator-install.mjs +30 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/indicator.mjs +17 -0
- package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +12 -6
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +5 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/indicator-command.mjs +14 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/indicator-text.mjs +13 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +2 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation-order.mjs +12 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation-writer.mjs +37 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation.mjs +47 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-indicator.mjs +141 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-scan.mjs +99 -0
- package/node_modules/@agents-can-communicate/cli/package.json +2 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +5 -3
- package/node_modules/@agents-can-communicate/cli/src/claim-command.mjs +64 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +15 -8
- package/node_modules/@agents-can-communicate/cli/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-command.mjs +65 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-previous-command.mjs +84 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-previous-windows.mjs +8 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator.mjs +112 -0
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +4 -0
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +8 -54
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +19 -0
- package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
- package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +2 -0
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/claim-selectors.mjs +62 -0
- package/node_modules/@agents-can-communicate/core/src/claims.mjs +88 -81
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +9 -6
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +19 -5
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +3 -0
- package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +1 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +2 -1
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +11 -2
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/claim-tools.mjs +56 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +9 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +7 -8
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +27 -9
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +13 -0
- package/package.json +3 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
2
|
+
|
|
3
|
+
export const unsafe = (reason = "unrecognized TOML declaration") => {
|
|
4
|
+
throw new AccError(EXIT.CONFLICT, `cannot safely edit status-line TOML: ${reason}; `
|
|
5
|
+
+ "repair ambiguous TOML before retrying", { reason });
|
|
6
|
+
};
|
|
7
|
+
// A bounded lexical scan, not a TOML value parser. Newlines inside strings,
|
|
8
|
+
// arrays and inline tables cannot introduce a header or a managed marker.
|
|
9
|
+
export function* statements(source) {
|
|
10
|
+
let start = 0, code = "", quote = null;
|
|
11
|
+
const stack = [];
|
|
12
|
+
for (let i = 0; i < source.length;) {
|
|
13
|
+
const char = source[i];
|
|
14
|
+
if (quote) {
|
|
15
|
+
if (quote[0] === '"' && char === "\\") {
|
|
16
|
+
code += source.slice(i, i + 2); i += 2; continue;
|
|
17
|
+
}
|
|
18
|
+
if (source.startsWith(quote, i)) {
|
|
19
|
+
let count = quote.length;
|
|
20
|
+
if (quote.length === 3) {
|
|
21
|
+
while (source[i + count] === quote[0]) count++;
|
|
22
|
+
if (count > 5) unsafe("unsupported string quote delimiter");
|
|
23
|
+
}
|
|
24
|
+
code += source.slice(i, i + count); i += count; quote = null; continue;
|
|
25
|
+
}
|
|
26
|
+
if (quote.length === 1 && /[\r\n]/.test(char)) unsafe("newline in single-line string");
|
|
27
|
+
code += char; i++; continue;
|
|
28
|
+
}
|
|
29
|
+
if (char === "#") {
|
|
30
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (char === '"' || char === "'") {
|
|
34
|
+
quote = source.startsWith(char.repeat(3), i) ? char.repeat(3) : char;
|
|
35
|
+
code += quote; i += quote.length; continue;
|
|
36
|
+
}
|
|
37
|
+
if (char === "[" || char === "{") stack.push(char);
|
|
38
|
+
if (char === "]" || char === "}") {
|
|
39
|
+
if (stack.pop() !== (char === "]" ? "[" : "{")) unsafe("mismatched collection delimiter");
|
|
40
|
+
}
|
|
41
|
+
code += char; i++;
|
|
42
|
+
if (char === "\n" && stack.length === 0) {
|
|
43
|
+
yield { raw: source.slice(start, i), code: code.trim(), start, end: i };
|
|
44
|
+
start = i; code = "";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (quote || stack.length) unsafe(quote ? "unclosed string" : "unclosed collection");
|
|
48
|
+
if (start < source.length) yield { raw: source.slice(start), code: code.trim(), start, end: source.length };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function decodeKey(key) {
|
|
52
|
+
if (key[0] === "'") return key.slice(1, -1);
|
|
53
|
+
if (key[0] !== '"') return key;
|
|
54
|
+
return key.slice(1, -1).replace(/\\(U[\da-fA-F]{8}|u[\da-fA-F]{4}|[btnfr"\\])|\\./g,
|
|
55
|
+
(match, escape) => {
|
|
56
|
+
if (!escape) unsafe("unsupported quoted-key escape");
|
|
57
|
+
if (/^[uU]/.test(escape)) {
|
|
58
|
+
const point = Number.parseInt(escape.slice(1), 16);
|
|
59
|
+
if (point > 0x10ffff || (point >= 0xd800 && point <= 0xdfff)) {
|
|
60
|
+
unsafe("invalid quoted-key Unicode escape");
|
|
61
|
+
}
|
|
62
|
+
return String.fromCodePoint(point);
|
|
63
|
+
}
|
|
64
|
+
return ({ b: "\b", t: "\t", n: "\n", f: "\f", r: "\r", '"': '"', "\\": "\\" })[escape];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function keyPath(code) {
|
|
69
|
+
const keys = [];
|
|
70
|
+
let rest = code.trimStart();
|
|
71
|
+
for (;;) {
|
|
72
|
+
const match = /^(?:[A-Za-z0-9_-]+|'[^'\r\n]*'|"(?:[^"\\\r\n]|\\[^\r\n])*")/.exec(rest);
|
|
73
|
+
if (!match) unsafe("unrecognized TOML key");
|
|
74
|
+
keys.push(decodeKey(match[0]));
|
|
75
|
+
rest = rest.slice(match[0].length).trimStart();
|
|
76
|
+
if (!rest.startsWith(".")) return { keys, rest };
|
|
77
|
+
rest = rest.slice(1).trimStart();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function declaration(code) {
|
|
82
|
+
const header = code.startsWith("[");
|
|
83
|
+
const array = header && code.startsWith("[[");
|
|
84
|
+
const { keys, rest } = keyPath(header ? code.slice(array ? 2 : 1) : code);
|
|
85
|
+
if (header ? rest !== (array ? "]]" : "]") : !rest.startsWith("=")) unsafe();
|
|
86
|
+
return { keys, header, array, value: header ? null : rest.slice(1).trim() };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Resolved declaration paths are only structural facts, never parsed user values.
|
|
90
|
+
export function scanConfig(source) {
|
|
91
|
+
let table = [];
|
|
92
|
+
return [...statements(source)].map(item => {
|
|
93
|
+
if (!item.code) return { ...item, table: [...table], keys: [] };
|
|
94
|
+
const entry = declaration(item.code);
|
|
95
|
+
if (entry.header) table = entry.keys;
|
|
96
|
+
return { ...item, ...entry, table: [...table],
|
|
97
|
+
keys: entry.header ? entry.keys : [...table, ...entry.keys] };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agents-can-communicate/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.mjs",
|
|
8
8
|
"./hook-support": "./src/hook-support.mjs",
|
|
9
|
+
"./indicator": "./src/indicator-command.mjs",
|
|
9
10
|
"./managed-entry": "./src/managed-runtime/entry.mjs",
|
|
10
11
|
"./managed-worker": "./src/managed-runtime/worker.mjs"
|
|
11
12
|
},
|
|
@@ -18,13 +18,15 @@ export const COMMANDS = Object.freeze({
|
|
|
18
18
|
work: { required: [], optional: ["session", "generation", "summary", "mode",
|
|
19
19
|
"state"], repeated: ["hint"], flags: ["clear"] },
|
|
20
20
|
claim: { required: ["resource"],
|
|
21
|
-
optional: ["session", "generation", "mode", "enforcement", "reason", "lease"]
|
|
21
|
+
optional: ["session", "generation", "mode", "enforcement", "reason", "lease"],
|
|
22
|
+
repeated: ["resource"] },
|
|
22
23
|
// Neither is required on its own, because either one names the claim: an id
|
|
23
24
|
// is precise, and a resource is what the caller typed to take it in the first
|
|
24
25
|
// place. Requiring the id meant a round trip through `acc status --json` to
|
|
25
26
|
// look up something the caller never chose.
|
|
26
27
|
release: { required: [],
|
|
27
|
-
optional: ["
|
|
28
|
+
optional: ["session", "generation", "authority", "reason"],
|
|
29
|
+
repeated: ["claim", "resource"], flags: ["all"] },
|
|
28
30
|
message: { required: ["subject", "body"],
|
|
29
31
|
optional: ["session", "generation", "type", "obligation", "client-message-id"],
|
|
30
32
|
repeated: ["to", "supersedes", "withdraws"] },
|
|
@@ -57,7 +59,7 @@ export const COMMANDS = Object.freeze({
|
|
|
57
59
|
// `--downgrade` because an older acc first on PATH will otherwise rewire every
|
|
58
60
|
// client to itself, and the only symptom is a guard behaving like the version
|
|
59
61
|
// it came from.
|
|
60
|
-
install: { required: [], optional: ["home", "delivery"], repeated: ["adapter"],
|
|
62
|
+
install: { required: [], optional: ["home", "delivery", "indicator"], repeated: ["adapter"],
|
|
61
63
|
flags: ["dry-run", "downgrade"] },
|
|
62
64
|
// `--dry-run` on both, because the preview was computed for either action and
|
|
63
65
|
// only `install` could ask for it. Removal is the side that reaches into a
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
2
|
+
|
|
3
|
+
import { positiveNumber } from "./args.mjs";
|
|
4
|
+
import { canonicalClaim } from "./claim-spelling.mjs";
|
|
5
|
+
|
|
6
|
+
const list = value => value === undefined ? undefined : Array.isArray(value) ? value : [value];
|
|
7
|
+
const owner = options => ({ sessionId: options.session, generation: options.generation });
|
|
8
|
+
|
|
9
|
+
export async function runClaimCommand({ options, context }) {
|
|
10
|
+
const resources = await Promise.all(list(options.resource)
|
|
11
|
+
.map(resource => canonicalClaim(resource, context.descriptor)));
|
|
12
|
+
const input = { ...owner(options), mode: options.mode ?? "exclusive",
|
|
13
|
+
enforcement: options.enforcement ?? "advisory", reason: options.reason ?? "unspecified",
|
|
14
|
+
leaseSeconds: options.lease ? positiveNumber(options.lease, "lease") : undefined,
|
|
15
|
+
descriptor: context.descriptor };
|
|
16
|
+
if (resources.length === 1) {
|
|
17
|
+
const claim = await context.service.acquireClaim({ ...input, resource: resources[0] });
|
|
18
|
+
return { data: claim, text: `claimed ${claim.resource}` };
|
|
19
|
+
}
|
|
20
|
+
const claims = await context.service.acquireClaims({ ...input, resources });
|
|
21
|
+
return { data: { claims }, text: `claimed ${claims.length} resources` };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Single-item authority keeps its existing lookup and ambiguity rules.
|
|
25
|
+
async function authorityClaim(resource, context) {
|
|
26
|
+
const { claims } = await context.service.collectStatus({});
|
|
27
|
+
const matching = claims.filter(claim => claim.resource === resource);
|
|
28
|
+
if (matching.length === 0) throw new AccError(EXIT.DATA, `no claim on ${resource} to release`,
|
|
29
|
+
{ resource, held: claims.map(claim => claim.resource) });
|
|
30
|
+
if (matching.length > 1) throw new AccError(EXIT.DATA,
|
|
31
|
+
`${matching.length} claims on ${resource}; name the one you mean with --claim`,
|
|
32
|
+
{ resource, claims: matching.map(claim => claim.claimId) });
|
|
33
|
+
return matching[0].claimId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function runReleaseCommand({ options, context }) {
|
|
37
|
+
const selectors = [options.claim, options.resource, options.all].filter(value => value !== undefined);
|
|
38
|
+
if (selectors.length !== 1) throw new AccError(EXIT.USAGE,
|
|
39
|
+
"release needs exactly one of --claim, --resource, or --all");
|
|
40
|
+
const claimIds = list(options.claim);
|
|
41
|
+
const resources = options.resource === undefined ? undefined
|
|
42
|
+
: await Promise.all(list(options.resource).map(resource => canonicalClaim(resource, context.descriptor)));
|
|
43
|
+
const scalar = options.all !== true && (claimIds ?? resources).length === 1;
|
|
44
|
+
if (options.authority !== undefined) {
|
|
45
|
+
if (!scalar) throw new AccError(EXIT.USAGE, "force-release authority requires a single claim");
|
|
46
|
+
const claimId = claimIds?.[0] ?? await authorityClaim(resources[0], context);
|
|
47
|
+
await context.service.forceReleaseClaim({ ...owner(options), claimId,
|
|
48
|
+
authority: options.authority, reason: options.reason });
|
|
49
|
+
return { data: { claimId }, text: `released ${claimId}` };
|
|
50
|
+
}
|
|
51
|
+
if (scalar && claimIds !== undefined) {
|
|
52
|
+
const claimId = claimIds[0];
|
|
53
|
+
await context.service.releaseClaim({ ...owner(options), claimId });
|
|
54
|
+
return { data: { claimId }, text: `released ${claimId}` };
|
|
55
|
+
}
|
|
56
|
+
const released = await context.service.releaseClaims({ ...owner(options),
|
|
57
|
+
...(options.all === true ? { all: true } : resources !== undefined ? { resources } : { claimIds }) });
|
|
58
|
+
if (scalar) {
|
|
59
|
+
const claimId = released[0].claimId;
|
|
60
|
+
return { data: { claimId }, text: `released ${claimId}` };
|
|
61
|
+
}
|
|
62
|
+
return { data: { claimIds: released.map(claim => claim.claimId) },
|
|
63
|
+
text: `released ${released.length} claims` };
|
|
64
|
+
}
|
|
@@ -33,8 +33,8 @@ const SUMMARY = Object.freeze({
|
|
|
33
33
|
prune: "report what this workspace no longer needs; --apply to reclaim it",
|
|
34
34
|
sync: "events since a cursor, or bounded message history",
|
|
35
35
|
work: "publish what this session is doing, or --clear when it has stopped",
|
|
36
|
-
claim: "reserve
|
|
37
|
-
release: "give
|
|
36
|
+
claim: "reserve resources atomically; exit 5 when a peer holds one",
|
|
37
|
+
release: "give selected claims back, or --all for this session",
|
|
38
38
|
ack: "acknowledge receipt without accepting work or writing an answer",
|
|
39
39
|
message: "send a typed message to named participants",
|
|
40
40
|
inbox: "list pending message headers; --message reads one complete message",
|
|
@@ -72,7 +72,7 @@ const CHOICES = Object.freeze({
|
|
|
72
72
|
finish: { status: HANDOFF_STATUSES },
|
|
73
73
|
message: { type: GENERIC_MESSAGE_KINDS, obligation: OBLIGATIONS },
|
|
74
74
|
sync: { scope: ["delta", "full", "history"], type: MESSAGE_KINDS },
|
|
75
|
-
install: { delivery: LIVE_POLICIES, adapter: ADAPTER_IDS },
|
|
75
|
+
install: { delivery: LIVE_POLICIES, adapter: ADAPTER_IDS, indicator: ["on", "off"] },
|
|
76
76
|
uninstall: { adapter: ADAPTER_IDS },
|
|
77
77
|
});
|
|
78
78
|
|
|
@@ -80,6 +80,7 @@ const NOTES = Object.freeze({
|
|
|
80
80
|
doctor: ["--repair completes recoverable writes without changing the store contract.",
|
|
81
81
|
"--migrate-store explicitly changes contract 6 to 7 after old clients and unmanaged store users stop; it cannot be combined with --repair."],
|
|
82
82
|
install: ["Without --delivery, one default-No question covers all selected clients that need a decision.",
|
|
83
|
+
"--indicator on|off controls the compact indicator in supported clients; omission preserves the saved choice (initially off).",
|
|
83
84
|
"--delivery actionable|all gives explicit complete-setup consent; --delivery off disables incoming automatic requests.",
|
|
84
85
|
"A dry run does not prompt, write configuration, or start a service. Service readiness does not establish a session binding."],
|
|
85
86
|
attach: ["Native hooks already manage their own session. Attach only for a manual CLI session.",
|
|
@@ -87,8 +88,12 @@ const NOTES = Object.freeze({
|
|
|
87
88
|
"--cadence is a positive interval in milliseconds."],
|
|
88
89
|
work: ["Provide --summary unless using --clear. Default mode: edit; default state: active."],
|
|
89
90
|
claim: ["--lease is in seconds. Default mode: exclusive; default enforcement: advisory.",
|
|
90
|
-
"
|
|
91
|
-
|
|
91
|
+
"Repeat --resource for an atomic batch. A conflict changes no claims, including existing leases.",
|
|
92
|
+
"For files, use file:src/a.mjs or file:src/**. Duplicate canonical resources are rejected."],
|
|
93
|
+
release: ["Choose repeated --claim, repeated --resource, or --all. Never combine selector families.",
|
|
94
|
+
"Explicit lists succeed completely or change no claims. --all releases only this session's claims and keeps it open.",
|
|
95
|
+
"An empty --all succeeds. A repeated explicit release of missing claims remains an error.",
|
|
96
|
+
"Releasing another owner's claim requires human or policy authority and a single selector."],
|
|
92
97
|
message: ["Default type: note. question/request require --to and obligation reply; note uses none.",
|
|
93
98
|
"decision permits none, or acknowledge when addressed. Use reply for answers and finish for handoffs.",
|
|
94
99
|
"For decisions, repeat --supersedes or --withdraws with 1..16 prior decision IDs; never combine the two.",
|
|
@@ -148,11 +153,13 @@ export function commandHelpText(command) {
|
|
|
148
153
|
const lines = [`acc ${name} - ${summary}`, "", `Usage: acc ${name}${suffix} [options]`, ""];
|
|
149
154
|
for (const [key, label] of [["required", "Required"], ["optional", "Optional"],
|
|
150
155
|
["repeated", "Repeatable"], ["flags", "Flags"]]) {
|
|
151
|
-
|
|
156
|
+
const options = command[key].filter(option => key !== "repeated" || !command.required.includes(option));
|
|
157
|
+
if (options.length === 0) continue;
|
|
152
158
|
lines.push(`${label}:`);
|
|
153
|
-
for (const option of
|
|
159
|
+
for (const option of options) {
|
|
154
160
|
const value = key === "flags" ? "" : ` <${choices[option]?.join("|") ?? "value"}>`;
|
|
155
|
-
|
|
161
|
+
const repeatable = key === "required" && command.repeated.includes(option) ? " (repeatable)" : "";
|
|
162
|
+
lines.push(` --${option}${value}${repeatable}`);
|
|
156
163
|
}
|
|
157
164
|
lines.push("");
|
|
158
165
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Composition root: discovery, runtime locations, and the CLI surface.
|
|
2
2
|
export { main } from "./main.mjs";
|
|
3
|
+
export { readIndicator } from "./indicator.mjs";
|
|
3
4
|
export { COMMANDS, parseArgs } from "./args.mjs";
|
|
4
5
|
export { askConfirmation } from "./confirm.mjs";
|
|
5
6
|
// Exported so a test can hold every adapter to where it plans to write and
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readTomlIndicatorSettings } from "@agents-can-communicate/adapter-sdk";
|
|
2
|
+
import { previousOutput } from "./indicator-previous-command.mjs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { readIndicator, indicatorFailure } from "./indicator.mjs";
|
|
5
|
+
import { ALL_ADAPTERS } from "./install-command.mjs";
|
|
6
|
+
|
|
7
|
+
function output(report, options, context) {
|
|
8
|
+
if (options.json) return JSON.stringify(report);
|
|
9
|
+
if (options.details) return [report.label, report.detail, report.action].filter(Boolean).join("\n");
|
|
10
|
+
const extension = context.extension;
|
|
11
|
+
let label = report.label;
|
|
12
|
+
try { label = extension?.renderText?.(report) ?? label; }
|
|
13
|
+
catch { /* A display extension cannot prevent the diagnostic. */ }
|
|
14
|
+
const indicator = label + (report.health === "problem" ? " · acc doctor" : "");
|
|
15
|
+
try {
|
|
16
|
+
if (extension?.composeText) return extension.composeText({ ...context, indicator });
|
|
17
|
+
} catch { /* Keep the diagnostic if the footer cannot be composed. */ }
|
|
18
|
+
return [context.previous, indicator].filter(Boolean).join("\n");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function input(stream) {
|
|
22
|
+
let source = "";
|
|
23
|
+
for await (const chunk of stream) {
|
|
24
|
+
source += chunk;
|
|
25
|
+
if (Buffer.byteLength(source) > 64 * 1024) throw new Error("indicator input too large");
|
|
26
|
+
}
|
|
27
|
+
return source;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runIndicator({ argv = process.argv.slice(2), stdin = process.stdin,
|
|
31
|
+
stdout = process.stdout } = {}) {
|
|
32
|
+
const options = {}, context = { payload: {}, previous: "", settings: {} };
|
|
33
|
+
let report = indicatorFailure(), timeout, expired = false;
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
try {
|
|
36
|
+
for (let i = 0; i < argv.length; i++) {
|
|
37
|
+
const flag = argv[i];
|
|
38
|
+
if (["--json", "--details"].includes(flag)) options[flag.slice(2)] = true;
|
|
39
|
+
else if (["--adapter", "--native-session", "--previous"].includes(flag)
|
|
40
|
+
&& typeof argv[i + 1] === "string") options[flag.slice(2)] = argv[++i];
|
|
41
|
+
else throw new Error("invalid indicator option");
|
|
42
|
+
}
|
|
43
|
+
context.extension = ALL_ADAPTERS().find(adapter => adapter.id === options.adapter)?.statusIndicator;
|
|
44
|
+
const plain = options.json || options.details;
|
|
45
|
+
timeout = setTimeout(() => {
|
|
46
|
+
expired = true;
|
|
47
|
+
controller.abort();
|
|
48
|
+
stdout.write(output(indicatorFailure(), options, context) + "\n", () => process.exit(0));
|
|
49
|
+
}, plain ? 1500 : context.extension?.budgetMs ?? 1500);
|
|
50
|
+
const source = options["native-session"] ? "{}" : await input(stdin);
|
|
51
|
+
context.payload = source.trim() ? JSON.parse(source) : {};
|
|
52
|
+
report = await readIndicator({ adapterId: options.adapter,
|
|
53
|
+
nativeSessionId: options["native-session"] ?? (context.extension?.nativeSessionId
|
|
54
|
+
? context.extension.nativeSessionId(context.payload)
|
|
55
|
+
: context.payload.conversation_id ?? context.payload.session_id) });
|
|
56
|
+
if (options.previous && !plain) {
|
|
57
|
+
const saved = JSON.parse(await readFile(options.previous, "utf8"));
|
|
58
|
+
context.settings = await readTomlIndicatorSettings(saved);
|
|
59
|
+
context.previous = await previousOutput(saved.previous, source,
|
|
60
|
+
context.extension?.previousTimeoutMs ?? 700, controller.signal);
|
|
61
|
+
}
|
|
62
|
+
} catch { /* A status renderer fails open with an actionable diagnostic. */ }
|
|
63
|
+
finally { clearTimeout(timeout); controller.abort(); }
|
|
64
|
+
if (!expired) stdout.write(output(report, options, context) + "\n");
|
|
65
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const LIMIT = 64 * 1024;
|
|
6
|
+
|
|
7
|
+
function stopTree(child) {
|
|
8
|
+
if (!Number.isInteger(child?.pid) || child.pid <= 0) return;
|
|
9
|
+
if (process.platform !== "win32") {
|
|
10
|
+
// detached creates a new group for this command, never for the host client.
|
|
11
|
+
try { process.kill(-child.pid, "SIGKILL"); } catch { /* Already gone. */ }
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
// Keep the owned launcher alive until taskkill discovers its descendants.
|
|
15
|
+
// Killing it first loses that relationship; an exited PID must never be reused.
|
|
16
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
17
|
+
const root = process.env.SystemRoot ?? process.env.SYSTEMROOT ?? "C:\\Windows";
|
|
18
|
+
return new Promise(resolve => {
|
|
19
|
+
// Launcher and cleanup must survive the outer deadline: libuv kills
|
|
20
|
+
// non-detached Windows children when their spawning Node process exits.
|
|
21
|
+
const killer = spawn(path.win32.join(root, "System32", "taskkill.exe"),
|
|
22
|
+
["/pid", String(child.pid), "/T", "/F"], { detached: true, windowsHide: true, stdio: "ignore" });
|
|
23
|
+
const timer = setTimeout(() => { killer.kill(); resolve(); }, 1000);
|
|
24
|
+
const done = () => { clearTimeout(timer); resolve(); };
|
|
25
|
+
killer.once("error", done);
|
|
26
|
+
killer.once("exit", done);
|
|
27
|
+
killer.unref();
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The original command receives the original stdin. Cancellation belongs to
|
|
32
|
+
// the indicator request; its timeout must not leave another polling process.
|
|
33
|
+
export async function previousOutput(previous, payload, timeout, signal) {
|
|
34
|
+
if (signal?.aborted || previous?.enabled === false
|
|
35
|
+
|| ["builtin", "disabled", "off", "none", "hidden"].includes(previous?.type)
|
|
36
|
+
|| typeof previous?.command !== "string" || !previous.command) return "";
|
|
37
|
+
return new Promise(resolve => {
|
|
38
|
+
let child, timer, settled = false, text = "";
|
|
39
|
+
const finish = () => {
|
|
40
|
+
if (settled) return;
|
|
41
|
+
settled = true;
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
signal?.removeEventListener("abort", finish);
|
|
44
|
+
// Continue draining stdout until Windows has stopped the tree. Closing
|
|
45
|
+
// it first can make the shell exit before taskkill inspects its children.
|
|
46
|
+
let cleanup;
|
|
47
|
+
try { cleanup = stopTree(child); } catch { /* Cleanup must fail open. */ }
|
|
48
|
+
Promise.resolve(cleanup).catch(() => {}).finally(() => {
|
|
49
|
+
child?.stdin?.destroy();
|
|
50
|
+
child?.stdout?.destroy();
|
|
51
|
+
if (child?.connected) child.disconnect();
|
|
52
|
+
child?.unref();
|
|
53
|
+
resolve(text.slice(0, LIMIT).trimEnd());
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
const windows = process.platform === "win32";
|
|
58
|
+
child = windows
|
|
59
|
+
? spawn(process.execPath, [fileURLToPath(new URL("./indicator-previous-windows.mjs", import.meta.url)), previous.command],
|
|
60
|
+
{ detached: true, stdio: ["pipe", "pipe", "ignore", "ipc"], windowsHide: true })
|
|
61
|
+
: spawn(previous.command, { shell: true, detached: true,
|
|
62
|
+
stdio: ["pipe", "pipe", "ignore"], windowsHide: true });
|
|
63
|
+
child.stdout.on("data", chunk => {
|
|
64
|
+
if (settled) return;
|
|
65
|
+
text += chunk;
|
|
66
|
+
if (Buffer.byteLength(text) > LIMIT) finish();
|
|
67
|
+
});
|
|
68
|
+
child.stdin.on("error", () => {});
|
|
69
|
+
child.once("error", finish);
|
|
70
|
+
child.once("close", finish);
|
|
71
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
72
|
+
// The command budget starts when the Windows launcher starts the shell.
|
|
73
|
+
// Startup is bounded too; the enclosing indicator deadline can abort both.
|
|
74
|
+
timer = setTimeout(finish, windows ? 1000 : timeout);
|
|
75
|
+
if (windows) child.once("message", message => {
|
|
76
|
+
if (settled || message !== "started") return;
|
|
77
|
+
clearTimeout(timer);
|
|
78
|
+
timer = setTimeout(finish, timeout);
|
|
79
|
+
});
|
|
80
|
+
child.stdin.end(payload);
|
|
81
|
+
if (signal?.aborted) finish();
|
|
82
|
+
} catch { finish(); }
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
// A detached cmd.exe loses its standard handles. Keep the owned Node launcher
|
|
4
|
+
// detached, and let a normal hidden shell inherit its original stdin/stdout.
|
|
5
|
+
const child = spawn(process.argv[2], { shell: true, windowsHide: true, stdio: "inherit" });
|
|
6
|
+
child.once("spawn", () => process.send?.("started", () => {}));
|
|
7
|
+
child.once("error", () => process.exit(0));
|
|
8
|
+
child.once("exit", () => process.exit(0));
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { assertPortableId } from "@agents-can-communicate/protocol";
|
|
4
|
+
import { effectiveCapabilities, loadNativeAttempt, loadSessionBinding, loadOfferObservation }
|
|
5
|
+
from "@agents-can-communicate/adapter-sdk";
|
|
6
|
+
import { readDeliveryBindingRecord, readSessionRecord } from "@agents-can-communicate/storage-filesystem";
|
|
7
|
+
import { readInstalledLivePolicyState } from "@agents-can-communicate/installer";
|
|
8
|
+
import { ALL_ADAPTERS } from "./install-command.mjs";
|
|
9
|
+
import { canonicalManagerRoot, managedDirectory, readManagedJson } from "./managed-runtime/state.mjs";
|
|
10
|
+
import { platformDataHome, runtimePaths } from "./runtime-paths.mjs";
|
|
11
|
+
|
|
12
|
+
const receptionText = {
|
|
13
|
+
automatic: "This chat can receive addressed requests without another user prompt. A busy client may queue them.",
|
|
14
|
+
turn: "Messages reach this chat on its next normal turn.",
|
|
15
|
+
inbox: "Messages are available when the agent reads acc inbox; no polling schedule is implied.",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function result({ health = "ready", reception = null, reasonCode = null,
|
|
19
|
+
detail, action = null, leaseCurrent = null }) {
|
|
20
|
+
const qualifier = reception && reception !== "automatic" ? ` · ${reception}` : "";
|
|
21
|
+
return { health, reception, reasonCode, leaseCurrent,
|
|
22
|
+
label: `ACC ${health === "ready" ? "●" : "!"}${qualifier}`,
|
|
23
|
+
detail: [detail, receptionText[reception]].filter(Boolean).join(" "), action };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const indicatorFailure = (reasonCode = "state_unreadable") => result({
|
|
27
|
+
health: "problem", reasonCode,
|
|
28
|
+
detail: reasonCode === "not_registered" ? "This chat is not registered with ACC."
|
|
29
|
+
: "ACC could not read this chat's local integration state.",
|
|
30
|
+
action: reasonCode === "not_registered"
|
|
31
|
+
? "Send a prompt to run the session hooks. If ACC is still absent, run acc doctor and check the plugin is enabled."
|
|
32
|
+
: "Run acc doctor in this project to identify and repair the local state problem.",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const actions = {
|
|
36
|
+
client_session_embedded: "This session has no shared delivery service. Run acc doctor to see the launch option and how to open a supported session.",
|
|
37
|
+
client_process_unknown: "Open a new client session so ACC can identify its process; then run acc doctor.",
|
|
38
|
+
transport_permission_denied: "Allow the sending command to access the local delivery channel; acc doctor shows the affected session.",
|
|
39
|
+
handshake_version_mismatch: "Update the client's delivery service to a supported version; acc doctor shows the version in use.",
|
|
40
|
+
native_policy_off: "Automatic delivery is disabled in ACC settings. Run acc install --delivery actionable to enable it.",
|
|
41
|
+
inbound_approval_required: "Approve the pending inbound request in the receiving client, or review its incoming-message setting.",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** A display observation, never CLI ownership. Native ids select one saved room
|
|
45
|
+
* and one binding; there is no roster scan, Git probe, store open, socket probe,
|
|
46
|
+
* lease refresh, heartbeat, update worker or managed-runtime process lease. */
|
|
47
|
+
export async function readIndicator({ adapterId, nativeSessionId,
|
|
48
|
+
dataHome = platformDataHome(), now = new Date().toISOString() }) {
|
|
49
|
+
try {
|
|
50
|
+
const adapter = ALL_ADAPTERS().find(item => item.id === adapterId);
|
|
51
|
+
if (!adapter || typeof nativeSessionId !== "string" || !nativeSessionId
|
|
52
|
+
|| nativeSessionId.length > 4096) return indicatorFailure("not_registered");
|
|
53
|
+
const key = createHash("sha256").update(JSON.stringify([adapterId, nativeSessionId])).digest("hex");
|
|
54
|
+
const accRoot = await canonicalManagerRoot(path.join(dataHome, "acc"));
|
|
55
|
+
const rooms = path.join(accRoot, "native-workspaces");
|
|
56
|
+
if (!await managedDirectory(rooms)) return indicatorFailure("not_registered");
|
|
57
|
+
const room = await readManagedJson(path.join(rooms, `${key}.json`));
|
|
58
|
+
if (!room) return indicatorFailure("not_registered");
|
|
59
|
+
if (room.schemaVersion !== 1 || room.adapterId !== adapterId
|
|
60
|
+
|| room.nativeSessionId !== nativeSessionId) throw new Error("invalid room");
|
|
61
|
+
assertPortableId(room.workspaceId, "workspace id");
|
|
62
|
+
const root = runtimePaths({ dataHome: path.dirname(accRoot), workspaceId: room.workspaceId }).root;
|
|
63
|
+
const owner = await loadSessionBinding({ runtimeDir: root, harnessSessionId: nativeSessionId });
|
|
64
|
+
if (!owner) return indicatorFailure("not_registered");
|
|
65
|
+
const input = { root, workspaceId: room.workspaceId, sessionId: owner.accSessionId };
|
|
66
|
+
const session = await readSessionRecord(input);
|
|
67
|
+
if (session?.state !== "open" || session.generation !== owner.generation
|
|
68
|
+
|| session.harness !== adapterId) return indicatorFailure("not_registered");
|
|
69
|
+
const [binding, handshake, policy, offer] = await Promise.all([
|
|
70
|
+
readDeliveryBindingRecord(input),
|
|
71
|
+
loadNativeAttempt({ runtimeDir: root, harnessSessionId: nativeSessionId, ...owner }),
|
|
72
|
+
readInstalledLivePolicyState({ dataHome: path.dirname(accRoot), adapterId }),
|
|
73
|
+
loadOfferObservation({ runtimeDir: root, sessionId: owner.accSessionId, generation: owner.generation }),
|
|
74
|
+
]);
|
|
75
|
+
const attempt = offer && (!handshake || Date.parse(offer.at) >= Date.parse(handshake.at)) ? offer : handshake;
|
|
76
|
+
const current = await readSessionRecord(input);
|
|
77
|
+
if (current?.state !== "open" || current.generation !== owner.generation) {
|
|
78
|
+
return indicatorFailure("not_registered");
|
|
79
|
+
}
|
|
80
|
+
const capabilities = effectiveCapabilities(adapter, {
|
|
81
|
+
clientName: owner.clientName ?? adapter.client.certificationName,
|
|
82
|
+
clientVersion: owner.clientVersion, platform: owner.platform,
|
|
83
|
+
});
|
|
84
|
+
const fallback = capabilities.delivery.nextTurn ? "turn" : "inbox";
|
|
85
|
+
const bound = binding?.generation === owner.generation && binding.adapterId === adapterId
|
|
86
|
+
&& binding.retiredAt == null && binding.availableModes.includes("livePush");
|
|
87
|
+
const leaseCurrent = bound ? Date.parse(binding.leaseUntil) > Date.parse(now) : null;
|
|
88
|
+
if (["missing", "invalid", "unavailable"].includes(policy.policyStatus)) {
|
|
89
|
+
return result({ health: "problem", reception: fallback, reasonCode: "installation_unavailable",
|
|
90
|
+
detail: "ACC cannot find a valid delivery setting for this integration.",
|
|
91
|
+
action: `Run acc install --adapter ${adapterId} to repair the integration.`, leaseCurrent });
|
|
92
|
+
}
|
|
93
|
+
if (policy.policy === "off") return result({ reception: fallback, reasonCode: "native_policy_off",
|
|
94
|
+
detail: "Automatic delivery is disabled in ACC settings.", leaseCurrent });
|
|
95
|
+
if (attempt?.state === "degraded") return result({ health: "problem", reception: fallback,
|
|
96
|
+
reasonCode: attempt.reasonCode ?? "handshake_failed", leaseCurrent,
|
|
97
|
+
detail: "ACC could not establish automatic delivery for this chat.",
|
|
98
|
+
action: actions[attempt.reasonCode]
|
|
99
|
+
?? "Run acc doctor in this project for the channel failure and the client's recovery steps.",
|
|
100
|
+
});
|
|
101
|
+
// Lease expiry alone is neither disconnection nor an operation in progress.
|
|
102
|
+
// The delivery router verifies the endpoint and renews it on the next send.
|
|
103
|
+
if (bound && binding.livePolicy !== "off") return result({ reception: "automatic", leaseCurrent });
|
|
104
|
+
if (attempt?.state === "unsupported" || !capabilities.delivery.livePush) {
|
|
105
|
+
return result({ reception: fallback, reasonCode: "native_delivery_unsupported",
|
|
106
|
+
detail: "This client session does not support automatic wake-up." });
|
|
107
|
+
}
|
|
108
|
+
return result({ health: "problem", reception: fallback, reasonCode: "native_unbound",
|
|
109
|
+
detail: "This chat has no automatic delivery channel.",
|
|
110
|
+
action: "Send a prompt to let ACC establish the channel; if it remains unavailable, run acc doctor." });
|
|
111
|
+
} catch { return indicatorFailure(); }
|
|
112
|
+
}
|
|
@@ -224,6 +224,9 @@ export function actedOn(result) {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
export async function runInstallCommand({ options, runtime, action = "install" }) {
|
|
227
|
+
if (options.indicator !== undefined && !["on", "off"].includes(options.indicator)) {
|
|
228
|
+
throw new AccError(EXIT.USAGE, "--indicator must be on or off");
|
|
229
|
+
}
|
|
227
230
|
const adapters = selectAdapters(options.adapter);
|
|
228
231
|
const home = options.home ?? runtime.env?.HOME ?? homedir();
|
|
229
232
|
const { data: dataHome } = platformPaths({ platform: runtime.platform,
|
|
@@ -261,6 +264,7 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
261
264
|
? await decideDelivery({ options, detected, recorded, runtime, dryRun })
|
|
262
265
|
: { deliveryByAdapter: {}, asked: [], notes: [] };
|
|
263
266
|
const plan = planInstallation({ adapters, detected, context, action, recorded,
|
|
267
|
+
indicator: options.indicator,
|
|
264
268
|
accVersion, allowDowngrade: options.downgrade === true, requested,
|
|
265
269
|
deliveryByAdapter: decided.deliveryByAdapter,
|
|
266
270
|
deliveryDecisionByAdapter: decided.deliveryDecisionByAdapter,
|