agents-can-communicate 0.10.4 → 0.11.1
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/bin/entrypoints/acc-antigravity-relay.mjs +2 -2
- package/bin/entrypoints/acc-hook.mjs +4 -24
- package/bin/entrypoints/acc-mcp.mjs +8 -6
- package/bin/entrypoints/acc.mjs +22 -11
- package/bin/entrypoints/antigravity-relay-binding.mjs +1 -1
- 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 +133 -0
- package/docs/TROUBLESHOOTING.md +2 -2
- 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 +83 -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 +29 -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-attempt-writer.mjs +1 -1
- 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/process-table.mjs +40 -27
- 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 +8 -1
- package/node_modules/@agents-can-communicate/cli/src/adapter-catalog.mjs +33 -0
- 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 +17 -11
- 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 +38 -110
- 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 +1 -1
- package/node_modules/@agents-can-communicate/cli/src/recorded-text.mjs +32 -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/core/src/next-turn-delivery.mjs +2 -1
- package/node_modules/@agents-can-communicate/core/src/sessions.mjs +5 -2
- package/node_modules/@agents-can-communicate/core/src/sync.mjs +2 -1
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +19 -7
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +23 -11
- package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +66 -26
- package/node_modules/@agents-can-communicate/installer/package.json +3 -2
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +3 -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 +3 -2
- 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/node_modules/@agents-can-communicate/storage-filesystem/src/state-reads.mjs +24 -7
- package/package.json +3 -1
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {mkdir,readFile,writeFile,rm} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {scanConfig,unsafe} from './toml-scan.mjs';
|
|
4
|
+
|
|
5
|
+
const read = async file => readFile(file,'utf8').catch(e=>{if(e.code==='ENOENT')return null;throw e;});
|
|
6
|
+
const equal = (a,b) => JSON.stringify(a)===JSON.stringify(b);
|
|
7
|
+
const prefix = (a,b) => b.every((item,i)=>a[i]===item);
|
|
8
|
+
|
|
9
|
+
// Only the status section's scalar values and string arrays are interpreted.
|
|
10
|
+
// Other TOML is scanned lexically and retained byte for byte.
|
|
11
|
+
function valueOf(source) {
|
|
12
|
+
let rest=source.trim();
|
|
13
|
+
function take() {
|
|
14
|
+
rest=rest.trimStart();
|
|
15
|
+
const quote=rest[0];
|
|
16
|
+
if(quote==='"'||quote==="'") {
|
|
17
|
+
const delimiter=rest.startsWith(quote.repeat(3))?quote.repeat(3):quote;
|
|
18
|
+
let i=delimiter.length, text='';
|
|
19
|
+
if(delimiter.length===3 && rest[i]==='\n')i++;
|
|
20
|
+
for(;i<rest.length;) {
|
|
21
|
+
if(rest.startsWith(delimiter,i)) {rest=rest.slice(i+delimiter.length);return text;}
|
|
22
|
+
if(quote==='"'&&rest[i]==='\\') {
|
|
23
|
+
const match=/^\\(U[\da-fA-F]{8}|u[\da-fA-F]{4}|[btnfr"\\])/.exec(rest.slice(i));
|
|
24
|
+
if(!match)unsafe('unsupported status string escape');
|
|
25
|
+
const escape=match[1];
|
|
26
|
+
text+=/^[uU]/.test(escape)?String.fromCodePoint(parseInt(escape.slice(1),16))
|
|
27
|
+
:({b:'\b',t:'\t',n:'\n',f:'\f',r:'\r','"':'"','\\':'\\'})[escape];
|
|
28
|
+
i+=match[0].length;
|
|
29
|
+
} else text+=rest[i++];
|
|
30
|
+
}
|
|
31
|
+
unsafe('unclosed status string');
|
|
32
|
+
}
|
|
33
|
+
if(rest[0]==='[') {
|
|
34
|
+
rest=rest.slice(1);const values=[];
|
|
35
|
+
while(!rest.trimStart().startsWith(']')) {
|
|
36
|
+
values.push(take());rest=rest.trimStart();
|
|
37
|
+
if(rest[0]!==',')break;
|
|
38
|
+
rest=rest.slice(1);
|
|
39
|
+
}
|
|
40
|
+
rest=rest.trimStart();if(rest[0]!==']')unsafe('invalid status array');
|
|
41
|
+
rest=rest.slice(1);return values;
|
|
42
|
+
}
|
|
43
|
+
const scalar=/^(true|false|[-+]?\d+(?:\.\d+)?)(?=\s|,|\]|$)/.exec(rest);
|
|
44
|
+
if(!scalar)unsafe('unsupported status value');
|
|
45
|
+
rest=rest.slice(scalar[0].length);
|
|
46
|
+
return scalar[0]==='true'?true:scalar[0]==='false'?false:Number(scalar[0]);
|
|
47
|
+
}
|
|
48
|
+
const result=take();if(rest.trim())unsafe('extra status value');return result;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function inspect(source,table) {
|
|
52
|
+
const nodes=scanConfig(source??''), entries={}, settings={};let header=null;
|
|
53
|
+
for(const node of nodes) {
|
|
54
|
+
if(!node.code)continue;
|
|
55
|
+
if(prefix(node.keys,table)&&node.keys.length>table.length+(node.header?0:1))unsafe('nested status declaration');
|
|
56
|
+
if(node.header&&equal(node.keys,table)) {
|
|
57
|
+
if(header||node.array)unsafe('duplicate status table');header=node;continue;
|
|
58
|
+
}
|
|
59
|
+
if(!node.header&&prefix(table,node.keys))unsafe('inline status table');
|
|
60
|
+
if(!node.header&&prefix(node.keys,table)&&node.keys.length===table.length+1) {
|
|
61
|
+
const key=node.keys.at(-1);
|
|
62
|
+
if(entries[key])unsafe('duplicate status key');
|
|
63
|
+
entries[key]=node;settings[key]=valueOf(node.value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if(Object.keys(entries).length&&!header)unsafe('use an explicit status table instead of dotted keys');
|
|
67
|
+
return {nodes,entries,settings,header};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function edit(source,table,view,replacements) {
|
|
71
|
+
let next=source??'';const patches=[];const additions=[];
|
|
72
|
+
for(const [key,raw] of Object.entries(replacements)) {
|
|
73
|
+
const entry=view.entries[key];
|
|
74
|
+
if(entry)patches.push({start:entry.start,end:entry.end,text:raw??''});
|
|
75
|
+
else if(raw)additions.push(raw);
|
|
76
|
+
}
|
|
77
|
+
if(additions.length) {
|
|
78
|
+
const at=view.header?.end??next.length;
|
|
79
|
+
const separator=at>0&&next[at-1]!=='\n'?'\n':'';
|
|
80
|
+
patches.push({start:at,end:at,text:separator+(view.header?'':`[${table.join('.')}]\n`)+additions.join('')});
|
|
81
|
+
}
|
|
82
|
+
for(const patch of patches.sort((a,b)=>b.start-a.start))next=next.slice(0,patch.start)+patch.text+next.slice(patch.end);
|
|
83
|
+
return next;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Save only this command's ownership; later user edits win over refresh/removal. */
|
|
87
|
+
export async function configureTomlIndicator({file,marker,table,enabled,set,defaults={},dryRun=false}) {
|
|
88
|
+
const source=await read(file),savedText=await read(marker);
|
|
89
|
+
const saved=savedText===null?null:JSON.parse(savedText);
|
|
90
|
+
if(!enabled&&!saved)return [];
|
|
91
|
+
if(saved&&(saved.schemaVersion!==1||!equal(saved.table,table)||typeof saved.installed?.command!=='string'
|
|
92
|
+
||!Array.isArray(saved.owned)||typeof saved.installedSource!=='string'))unsafe('invalid indicator ownership');
|
|
93
|
+
const view=inspect(source,table);
|
|
94
|
+
if(dryRun)return [];
|
|
95
|
+
if(saved&&view.settings.command!==saved.installed.command) {
|
|
96
|
+
if(!enabled)await rm(marker,{force:true});
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
if(!enabled) {
|
|
100
|
+
let next;
|
|
101
|
+
if(source===saved.installedSource)next=saved.original;
|
|
102
|
+
else {
|
|
103
|
+
const replacements={};
|
|
104
|
+
for(const key of saved.owned)if(equal(view.settings[key],saved.installed[key]))replacements[key]=saved.raw[key]??null;
|
|
105
|
+
next=edit(source,table,view,replacements);
|
|
106
|
+
if(!saved.hadHeader) {
|
|
107
|
+
const after=inspect(next,table);
|
|
108
|
+
if(after.header&&Object.keys(after.entries).length===0)next=next.slice(0,after.header.start)+next.slice(after.header.end);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if(next===null)await rm(file,{force:true});else if(next!==source)await writeFile(file,next);
|
|
112
|
+
await rm(marker,{force:true});return [file];
|
|
113
|
+
}
|
|
114
|
+
const previous=saved?.previous??view.settings;
|
|
115
|
+
const values=typeof set==='function'?await set():set;
|
|
116
|
+
const installed={...view.settings,...Object.fromEntries(Object.entries(defaults).filter(([key])=>!Object.hasOwn(view.settings,key))),...values};
|
|
117
|
+
const owned=saved?saved.owned.filter(key=>equal(view.settings[key],saved.installed[key]))
|
|
118
|
+
:Object.keys(installed).filter(key=>!equal(installed[key],previous[key]));
|
|
119
|
+
const changes={};
|
|
120
|
+
for(const key of owned)if(!equal(view.settings[key],installed[key]))changes[key]=`${key} = ${JSON.stringify(installed[key])}\n`;
|
|
121
|
+
const next=edit(source,table,view,changes);
|
|
122
|
+
const record={schemaVersion:1,file,table,previous,installed,owned,
|
|
123
|
+
raw:saved?.raw??Object.fromEntries(Object.entries(view.entries).map(([key,node])=>[key,node.raw])),
|
|
124
|
+
original:saved?saved.original:source,hadHeader:saved?.hadHeader??Boolean(view.header),
|
|
125
|
+
installedSource:saved?.installedSource??next};
|
|
126
|
+
await mkdir(path.dirname(marker),{recursive:true});
|
|
127
|
+
await writeFile(marker,JSON.stringify(record)+'\n',{mode:0o600});
|
|
128
|
+
try {
|
|
129
|
+
if(next!==source){await mkdir(path.dirname(file),{recursive:true});await writeFile(file,next);}
|
|
130
|
+
} catch(error) {
|
|
131
|
+
if(savedText===null)await rm(marker,{force:true});else await writeFile(marker,savedText,{mode:0o600});
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
return [file];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Read current display choices without reclaiming the previous shell program. */
|
|
138
|
+
export async function readTomlIndicatorSettings(record) {
|
|
139
|
+
if(typeof record.file!=='string'||!Array.isArray(record.table))return record.previous??{};
|
|
140
|
+
return inspect(await read(record.file),record.table).settings;
|
|
141
|
+
}
|
|
@@ -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,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agents-can-communicate/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.mjs",
|
|
8
|
+
"./adapters": "./src/adapter-catalog.mjs",
|
|
9
|
+
"./confirm": "./src/confirm.mjs",
|
|
10
|
+
"./git-probe": "./src/git-probe.mjs",
|
|
11
|
+
"./main": "./src/main.mjs",
|
|
12
|
+
"./runtime-paths": "./src/runtime-paths.mjs",
|
|
13
|
+
"./workspace-discovery": "./src/workspace-discovery.mjs",
|
|
8
14
|
"./hook-support": "./src/hook-support.mjs",
|
|
15
|
+
"./indicator": "./src/indicator-command.mjs",
|
|
9
16
|
"./managed-entry": "./src/managed-runtime/entry.mjs",
|
|
10
17
|
"./managed-worker": "./src/managed-runtime/worker.mjs"
|
|
11
18
|
},
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Every client adapter by id, loaded only by the command or hook that serves
|
|
2
|
+
// it. Importing all six cost every `acc version`, indicator read and hook their
|
|
3
|
+
// module graphs (#270), so this module imports none of them until asked.
|
|
4
|
+
const LOADERS = Object.freeze({
|
|
5
|
+
// Registered like any other client. Its hook commands carry an extra
|
|
6
|
+
// argument - the event name - because this client's payload has none.
|
|
7
|
+
antigravity: () => import("@agents-can-communicate/adapter-antigravity")
|
|
8
|
+
.then(module => module.createAntigravityAdapter()),
|
|
9
|
+
claude_code: () => import("@agents-can-communicate/adapter-claude-code")
|
|
10
|
+
.then(module => module.createClaudeCodeAdapter()),
|
|
11
|
+
codex: () => import("@agents-can-communicate/adapter-codex")
|
|
12
|
+
.then(module => module.createCodexAdapter()),
|
|
13
|
+
gemini_cli: () => import("@agents-can-communicate/adapter-gemini-cli")
|
|
14
|
+
.then(module => module.createGeminiCliAdapter()),
|
|
15
|
+
grok: () => import("@agents-can-communicate/adapter-grok")
|
|
16
|
+
.then(module => module.createGrokAdapter()),
|
|
17
|
+
kimi: () => import("@agents-can-communicate/adapter-kimi")
|
|
18
|
+
.then(module => module.createKimiAdapter()),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export const ADAPTER_IDS = Object.freeze(Object.keys(LOADERS));
|
|
22
|
+
|
|
23
|
+
/** The adapter for one id, or null for an id no adapter has. */
|
|
24
|
+
export async function loadAdapter(adapterId) {
|
|
25
|
+
return Object.hasOwn(LOADERS, adapterId) ? LOADERS[adapterId]() : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The adapters for these ids, keyed by id; unknown ids are left out. */
|
|
29
|
+
export async function loadAdapters(adapterIds) {
|
|
30
|
+
const known = [...new Set(adapterIds)].filter(adapterId => Object.hasOwn(LOADERS, adapterId));
|
|
31
|
+
return Object.fromEntries(await Promise.all(known.map(async adapterId =>
|
|
32
|
+
[adapterId, await LOADERS[adapterId]()])));
|
|
33
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { CLAIM_ENFORCEMENTS, CLAIM_MODES, GENERIC_MESSAGE_KINDS, HANDOFF_STATUSES,
|
|
2
2
|
INTENT_MODES, INTENT_STATES, MESSAGE_KINDS, OBLIGATIONS } from "@agents-can-communicate/protocol";
|
|
3
|
-
import { LIVE_POLICIES } from "@agents-can-communicate/installer";
|
|
3
|
+
import { LIVE_POLICIES } from "@agents-can-communicate/installer/live-policy";
|
|
4
4
|
|
|
5
5
|
import { COMMANDS, commandSpec } from "./args.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { ADAPTER_IDS } from "./adapter-catalog.mjs";
|
|
7
7
|
import { needsOwner } from "./session-owner.mjs";
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -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",
|
|
@@ -65,14 +65,13 @@ export function describeCommands() {
|
|
|
65
65
|
const DOCS = "https://github.com/automatis-tools/agents-can-communicate"
|
|
66
66
|
+ "/blob/main/docs/CLI.md";
|
|
67
67
|
|
|
68
|
-
const ADAPTER_IDS = Object.freeze(ALL_ADAPTERS().map(adapter => adapter.id));
|
|
69
68
|
const CHOICES = Object.freeze({
|
|
70
69
|
work: { mode: INTENT_MODES, state: INTENT_STATES },
|
|
71
70
|
claim: { mode: CLAIM_MODES, enforcement: CLAIM_ENFORCEMENTS },
|
|
72
71
|
finish: { status: HANDOFF_STATUSES },
|
|
73
72
|
message: { type: GENERIC_MESSAGE_KINDS, obligation: OBLIGATIONS },
|
|
74
73
|
sync: { scope: ["delta", "full", "history"], type: MESSAGE_KINDS },
|
|
75
|
-
install: { delivery: LIVE_POLICIES, adapter: ADAPTER_IDS },
|
|
74
|
+
install: { delivery: LIVE_POLICIES, adapter: ADAPTER_IDS, indicator: ["on", "off"] },
|
|
76
75
|
uninstall: { adapter: ADAPTER_IDS },
|
|
77
76
|
});
|
|
78
77
|
|
|
@@ -80,6 +79,7 @@ const NOTES = Object.freeze({
|
|
|
80
79
|
doctor: ["--repair completes recoverable writes without changing the store contract.",
|
|
81
80
|
"--migrate-store explicitly changes contract 6 to 7 after old clients and unmanaged store users stop; it cannot be combined with --repair."],
|
|
82
81
|
install: ["Without --delivery, one default-No question covers all selected clients that need a decision.",
|
|
82
|
+
"--indicator on|off controls the compact indicator in supported clients; omission preserves the saved choice (initially off).",
|
|
83
83
|
"--delivery actionable|all gives explicit complete-setup consent; --delivery off disables incoming automatic requests.",
|
|
84
84
|
"A dry run does not prompt, write configuration, or start a service. Service readiness does not establish a session binding."],
|
|
85
85
|
attach: ["Native hooks already manage their own session. Attach only for a manual CLI session.",
|
|
@@ -87,8 +87,12 @@ const NOTES = Object.freeze({
|
|
|
87
87
|
"--cadence is a positive interval in milliseconds."],
|
|
88
88
|
work: ["Provide --summary unless using --clear. Default mode: edit; default state: active."],
|
|
89
89
|
claim: ["--lease is in seconds. Default mode: exclusive; default enforcement: advisory.",
|
|
90
|
-
"
|
|
91
|
-
|
|
90
|
+
"Repeat --resource for an atomic batch. A conflict changes no claims, including existing leases.",
|
|
91
|
+
"For files, use file:src/a.mjs or file:src/**. Duplicate canonical resources are rejected."],
|
|
92
|
+
release: ["Choose repeated --claim, repeated --resource, or --all. Never combine selector families.",
|
|
93
|
+
"Explicit lists succeed completely or change no claims. --all releases only this session's claims and keeps it open.",
|
|
94
|
+
"An empty --all succeeds. A repeated explicit release of missing claims remains an error.",
|
|
95
|
+
"Releasing another owner's claim requires human or policy authority and a single selector."],
|
|
92
96
|
message: ["Default type: note. question/request require --to and obligation reply; note uses none.",
|
|
93
97
|
"decision permits none, or acknowledge when addressed. Use reply for answers and finish for handoffs.",
|
|
94
98
|
"For decisions, repeat --supersedes or --withdraws with 1..16 prior decision IDs; never combine the two.",
|
|
@@ -148,11 +152,13 @@ export function commandHelpText(command) {
|
|
|
148
152
|
const lines = [`acc ${name} - ${summary}`, "", `Usage: acc ${name}${suffix} [options]`, ""];
|
|
149
153
|
for (const [key, label] of [["required", "Required"], ["optional", "Optional"],
|
|
150
154
|
["repeated", "Repeatable"], ["flags", "Flags"]]) {
|
|
151
|
-
|
|
155
|
+
const options = command[key].filter(option => key !== "repeated" || !command.required.includes(option));
|
|
156
|
+
if (options.length === 0) continue;
|
|
152
157
|
lines.push(`${label}:`);
|
|
153
|
-
for (const option of
|
|
158
|
+
for (const option of options) {
|
|
154
159
|
const value = key === "flags" ? "" : ` <${choices[option]?.join("|") ?? "value"}>`;
|
|
155
|
-
|
|
160
|
+
const repeatable = key === "required" && command.repeated.includes(option) ? " (repeatable)" : "";
|
|
161
|
+
lines.push(` --${option}${value}${repeatable}`);
|
|
156
162
|
}
|
|
157
163
|
lines.push("");
|
|
158
164
|
}
|
|
@@ -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 { loadAdapter } from "./adapter-catalog.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 = (await loadAdapter(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));
|