agents-can-communicate 0.1.10 → 0.1.12
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/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/src/adapter.mjs +4 -2
- package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +38 -37
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +33 -7
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +10 -0
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +36 -2
- package/node_modules/@agents-can-communicate/core/package.json +1 -1
- 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/detect.mjs +5 -1
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/package.json +1 -1
|
@@ -68,8 +68,10 @@ export function createCodexAdapter() {
|
|
|
68
68
|
"write guards cover apply_patch and the shell writes ACC can read; a model "
|
|
69
69
|
+ "without apply_patch edits through the shell, where a redirection or an "
|
|
70
70
|
+ "mv is matched and a runtime opening the file is not",
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
// Was a standing sentence here, true and useless: it said the same
|
|
72
|
+
// thing on a machine whose hooks were trusted, on one whose were not,
|
|
73
|
+
// and on one with nothing installed. Detection reads the client's own
|
|
74
|
+
// record now and speaks only when it has something to report.
|
|
73
75
|
],
|
|
74
76
|
};
|
|
75
77
|
},
|
|
@@ -238,33 +238,6 @@ async function removeEmptyDirs(directories) {
|
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
/**
|
|
242
|
-
* Drop the client's trust record for hooks that are about to stop existing.
|
|
243
|
-
*
|
|
244
|
-
* Scoped to tables whose key begins with ACC's own `plugin@marketplace:` - a
|
|
245
|
-
* table belonging to any other plugin is the client's business and stays. The
|
|
246
|
-
* file is rewritten only when something was found, so an install that never ran
|
|
247
|
-
* here leaves the config byte for byte as it was.
|
|
248
|
-
*/
|
|
249
|
-
export async function removeHookTrust(file, prefix) {
|
|
250
|
-
const before = await readFile(file, "utf8").catch(() => null);
|
|
251
|
-
if (before === null || !before.includes(`hooks.state."${prefix}`)) return false;
|
|
252
|
-
|
|
253
|
-
const lines = before.split("\n");
|
|
254
|
-
const kept = [];
|
|
255
|
-
let dropping = false;
|
|
256
|
-
for (const line of lines) {
|
|
257
|
-
const header = /^\s*\[([^\]]*)\]\s*$/.exec(line);
|
|
258
|
-
if (header !== null) dropping = header[1].startsWith(`hooks.state."${prefix}`);
|
|
259
|
-
if (dropping) continue;
|
|
260
|
-
kept.push(line);
|
|
261
|
-
}
|
|
262
|
-
// A table's trailing blank line goes with it rather than piling up.
|
|
263
|
-
const text = kept.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
264
|
-
if (text === before) return false;
|
|
265
|
-
await writeFile(file, text, "utf8");
|
|
266
|
-
return true;
|
|
267
|
-
}
|
|
268
241
|
|
|
269
242
|
export async function uninstallCodexPlugin({ home, agentsHome = home,
|
|
270
243
|
codexHome = path.join(home, ".codex"), keep = [] }) {
|
|
@@ -277,15 +250,18 @@ export async function uninstallCodexPlugin({ home, agentsHome = home,
|
|
|
277
250
|
await writeMarketplace(file, { ...existing, plugins: kept });
|
|
278
251
|
}
|
|
279
252
|
if (await removeTomlBlock(configPath(codexHome))) changes.push(configPath(codexHome));
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
253
|
+
// The client's `[hooks.state."<plugin>:…"]` tables stay. 0.1.9 removed them as
|
|
254
|
+
// litter naming a plugin that was gone; that was wrong, and wrong in a way
|
|
255
|
+
// worth writing down. The check behind it perturbed the record - a hook whose
|
|
256
|
+
// recorded hash no longer *matched* was seen still running - and concluded the
|
|
257
|
+
// record was inert. Absence was never tested, and absence is what matters:
|
|
258
|
+
// with the tables gone this client runs no hook at all, prints
|
|
259
|
+
// `hook: SessionStart Completed` while executing nothing, and ACC's write
|
|
260
|
+
// guard is silently off while `acc doctor` reports the adapter installed.
|
|
261
|
+
//
|
|
262
|
+
// The record is granted once, by a person, and survives ACC upgrades - hashes
|
|
263
|
+
// captured under 0.1.6 still admitted 0.1.10's hooks. Nothing ACC writes can
|
|
264
|
+
// put it back. So it is not ACC's to remove.
|
|
289
265
|
// The marketplace directory is ACC's too, so it goes rather than being left
|
|
290
266
|
// behind empty.
|
|
291
267
|
// A blank TOML config and an absent one are the same to this client, and a
|
|
@@ -328,6 +304,13 @@ export async function detectCodex({ home, agentsHome = home,
|
|
|
328
304
|
const enabled = config.includes(`[plugins."${QUALIFIED}"]`);
|
|
329
305
|
const cached = await stat(cachePath(codexHome))
|
|
330
306
|
.then(() => true).catch(() => false);
|
|
307
|
+
// The condition that decides whether ACC runs here at all, and the only one
|
|
308
|
+
// this client will not tell you about: with no trust record it runs no hook,
|
|
309
|
+
// prints `hook: SessionStart Completed`, and executes nothing. Everything else
|
|
310
|
+
// - published, registered, enabled, cached - can be true while the write guard
|
|
311
|
+
// is off. Only asked when something is wired: a warning about hooks that do
|
|
312
|
+
// not exist is how a real one gets ignored.
|
|
313
|
+
const untrusted = cached && !config.includes(`hooks.state."${QUALIFIED}:`);
|
|
331
314
|
return { ok: true, changes: [], diagnostics: [
|
|
332
315
|
published ? "acc plugin published in the marketplace" : "acc plugin not registered",
|
|
333
316
|
registered && enabled
|
|
@@ -339,7 +322,25 @@ export async function detectCodex({ home, agentsHome = home,
|
|
|
339
322
|
cached
|
|
340
323
|
? "plugin installed in the client's cache"
|
|
341
324
|
: `plugin not installed yet; run: codex plugin add ${QUALIFIED}`,
|
|
342
|
-
|
|
325
|
+
// The condition that decides whether ACC runs here at all, and the only one
|
|
326
|
+
// this client will not tell you about: with no trust record it runs no hook,
|
|
327
|
+
// prints `hook: SessionStart Completed`, and executes nothing. Everything
|
|
328
|
+
// else - published, registered, enabled, cached - can be true while the
|
|
329
|
+
// write guard is off. Said only when there is something to trust, because a
|
|
330
|
+
// warning on a machine with nothing wired is how a real one gets ignored.
|
|
331
|
+
...(untrusted
|
|
332
|
+
? ["hooks are not trusted, so this client reports them completed and runs "
|
|
333
|
+
+ "nothing"]
|
|
334
|
+
: []),
|
|
335
|
+
],
|
|
336
|
+
// Said as an action rather than an observation, because a person has to do it
|
|
337
|
+
// and no acc command can: the client grants this once, from its own prompt.
|
|
338
|
+
// A diagnostic alone would have stayed in `--json`, which is where the first
|
|
339
|
+
// version of this fix put it and where nobody would have read it.
|
|
340
|
+
needsAction: untrusted
|
|
341
|
+
? ["start codex once and accept the hook trust prompt "
|
|
342
|
+
+ "# its hooks run nothing until then"]
|
|
343
|
+
: [] };
|
|
343
344
|
}
|
|
344
345
|
|
|
345
346
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
@@ -130,6 +130,9 @@ async function diagnoseAdapters({ options, runtime }) {
|
|
|
130
130
|
if (owned.missing.length > 0) {
|
|
131
131
|
remediation.push(`acc install --adapter ${entry.adapterId} # files are missing`);
|
|
132
132
|
}
|
|
133
|
+
// An adapter can name something only a person can do - the client's own
|
|
134
|
+
// trust prompt, most of all. It goes where a person reads, not into --json.
|
|
135
|
+
remediation.push(...(entry.needsAction ?? []));
|
|
133
136
|
const installed = record.installs.find(one => one.adapterId === entry.adapterId);
|
|
134
137
|
const stale = staleInstall({ recorded: installed?.accVersion ?? null, running });
|
|
135
138
|
if (stale !== null) {
|
|
@@ -153,9 +156,21 @@ async function diagnoseAdapters({ options, runtime }) {
|
|
|
153
156
|
|
|
154
157
|
export async function runDoctor({ options, context, runtime }) {
|
|
155
158
|
const root = context.paths.root;
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
// The clock comes from the context rather than through the service, because
|
|
160
|
+
// the service is what may not open: this command runs before anything reads a
|
|
161
|
+
// record, which is the whole point of it.
|
|
162
|
+
const clock = context.clock ?? context.service?.clock;
|
|
163
|
+
// A store that is not there yet is not an ambiguous one. Opening the workspace
|
|
164
|
+
// used to happen first and created it, so the inspection never saw this case;
|
|
165
|
+
// now that the diagnosis runs first, "no protocol.json" would read as
|
|
166
|
+
// "unreadable protocol.json" and a person's first `acc doctor` in a new
|
|
167
|
+
// project would answer that their store is broken.
|
|
168
|
+
const started = await stat(path.join(root, "protocol.json")).then(() => true, () => false);
|
|
169
|
+
const report = !started
|
|
170
|
+
? { healthy: true, blocked: [], corrupt: [], repaired: [] }
|
|
171
|
+
: options.repair === true
|
|
172
|
+
? await repairFilesystemStore({ root, clock })
|
|
173
|
+
: await diagnoseFilesystemStore({ root });
|
|
159
174
|
const adapters = await diagnoseAdapters({ options, runtime });
|
|
160
175
|
const { data: dataHome } = platformPaths({ platform: runtime?.platform,
|
|
161
176
|
env: runtime?.env ?? {} });
|
|
@@ -163,7 +178,7 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
163
178
|
? await runtime.version().catch(() => null)
|
|
164
179
|
: null;
|
|
165
180
|
const update = await noticeUpdate({ dataHome, running, env: runtime?.env ?? {},
|
|
166
|
-
now: Date.parse(
|
|
181
|
+
now: Date.parse(clock.now()), get: runtime?.fetch,
|
|
167
182
|
io: { readFile, writeFile, mkdir } });
|
|
168
183
|
|
|
169
184
|
// Before the store is read for anything else. `collectStatus` reads every
|
|
@@ -182,7 +197,11 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
182
197
|
remediation: ["inspect blocked and corrupt paths before repairing"] });
|
|
183
198
|
}
|
|
184
199
|
|
|
185
|
-
|
|
200
|
+
// Only now, and only because the report said the records can be read. A
|
|
201
|
+
// context built for this command opens the store on request rather than up
|
|
202
|
+
// front.
|
|
203
|
+
const service = context.service ?? await context.openService();
|
|
204
|
+
const status = await service.collectStatus({});
|
|
186
205
|
|
|
187
206
|
const data = {
|
|
188
207
|
workspaceId: context.descriptor.id,
|
|
@@ -207,6 +226,13 @@ export async function runDoctor({ options, context, runtime }) {
|
|
|
207
226
|
// the store was healthy and nothing else.
|
|
208
227
|
const text = [`store healthy; ${describePresence(status.counts)}; `
|
|
209
228
|
+ `protection ${status.protection}; ${installed} of ${adapters.length} adapter(s) installed`,
|
|
210
|
-
...data.remediation.map(line => ` ${line}`)
|
|
229
|
+
...data.remediation.map(line => ` ${line}`),
|
|
230
|
+
// `0 of 4` is a true line that reads as a broken machine, and on an
|
|
231
|
+
// MCP-only one it would read that way on every run forever. The server needs
|
|
232
|
+
// no adapter: measured answering `tools/list` and writing an intent on a
|
|
233
|
+
// machine with no client binaries on PATH at all.
|
|
234
|
+
...(installed === 0
|
|
235
|
+
? [" no client is wired; any MCP client can still take part through acc-mcp"]
|
|
236
|
+
: [])].join("\n");
|
|
211
237
|
return { data, text };
|
|
212
238
|
}
|
|
@@ -125,6 +125,16 @@ export function describeOutcome({ action, acted, failed = [], skipped = [],
|
|
|
125
125
|
// Said once, where it is needed: the reader has just been shown a list of
|
|
126
126
|
// their own files with ACC's name in them.
|
|
127
127
|
...(action === "install" && acted > 0 ? ["", "undo with: acc uninstall"] : []),
|
|
128
|
+
// And said once where it is needed differently: an install that wired nothing
|
|
129
|
+
// has just told this reader four times that their machine is not supported.
|
|
130
|
+
// It is not - the MCP server needs no adapter, and was measured answering on a
|
|
131
|
+
// machine with no client binaries at all. Only when nothing was wired: the
|
|
132
|
+
// skips already carry their own remedy, and a line printed every time is a
|
|
133
|
+
// line that stops being read.
|
|
134
|
+
...(action === "install" && acted === 0
|
|
135
|
+
? ["", "no client was wired, but any MCP client can still take part: "
|
|
136
|
+
+ "point it at the acc-mcp command"]
|
|
137
|
+
: []),
|
|
128
138
|
].join("\n");
|
|
129
139
|
}
|
|
130
140
|
|
|
@@ -54,7 +54,14 @@ async function claimOn(options, context) {
|
|
|
54
54
|
return mine[0].claimId;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Where this workspace's state lives, without reading a byte of it.
|
|
59
|
+
*
|
|
60
|
+
* Split out because `doctor` is the command a person runs when the store cannot
|
|
61
|
+
* be read, and it has to know which store to look at before deciding whether
|
|
62
|
+
* looking is safe.
|
|
63
|
+
*/
|
|
64
|
+
async function locateContext(options, runtime) {
|
|
58
65
|
const descriptor = await discoverWorkspace({
|
|
59
66
|
cwd: options.cwd ?? runtime.cwd,
|
|
60
67
|
env: runtime.env,
|
|
@@ -67,12 +74,37 @@ async function openContext(options, runtime) {
|
|
|
67
74
|
workspaceId: descriptor.id,
|
|
68
75
|
workspaceRoots: descriptor.roots,
|
|
69
76
|
});
|
|
77
|
+
return { descriptor, paths };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function withService({ descriptor, paths }, runtime) {
|
|
70
81
|
const store = await openFilesystemStore({ root: paths.root, clock: runtime.clock,
|
|
71
82
|
ids: runtime.ids, workspaceId: descriptor.id });
|
|
72
83
|
return { descriptor, paths,
|
|
73
84
|
service: createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids }) };
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
async function openContext(options, runtime) {
|
|
88
|
+
return withService(await locateContext(options, runtime), runtime);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A context for the one command that has to survive an unopenable store.
|
|
93
|
+
*
|
|
94
|
+
* `runDoctor` already refused to read records before diagnosing them - fixed
|
|
95
|
+
* after a truncated file made it answer "invalid JSON record" while the
|
|
96
|
+
* inspection had already found the file and put it in a list nobody saw. The
|
|
97
|
+
* throw then moved earlier: opening the store happens before any handler runs,
|
|
98
|
+
* so one unreadable `protocol.json` killed the diagnosis a frame before the code
|
|
99
|
+
* written to report it. Here the store is opened on request, after the report
|
|
100
|
+
* has said that reading is safe.
|
|
101
|
+
*/
|
|
102
|
+
async function openDiagnosticContext(options, runtime) {
|
|
103
|
+
const located = await locateContext(options, runtime);
|
|
104
|
+
return { ...located, clock: runtime.clock,
|
|
105
|
+
openService: async () => (await withService(located, runtime)).service };
|
|
106
|
+
}
|
|
107
|
+
|
|
76
108
|
const human = value => (typeof value === "string" ? value : JSON.stringify(value, null, 2));
|
|
77
109
|
|
|
78
110
|
/** How many are here, and how many only look it. */
|
|
@@ -368,7 +400,9 @@ export async function main(argv, runtime) {
|
|
|
368
400
|
// and `help` has to answer in a directory that is no workspace at all.
|
|
369
401
|
const context = NO_WORKSPACE.includes(parsed.command)
|
|
370
402
|
? null
|
|
371
|
-
:
|
|
403
|
+
: parsed.command === "doctor"
|
|
404
|
+
? await openDiagnosticContext(parsed.options, runtime)
|
|
405
|
+
: await openContext(parsed.options, runtime);
|
|
372
406
|
// Which session is calling is answered once, here, rather than by each
|
|
373
407
|
// handler: every one of them needs the same pair, and a handler that forgot
|
|
374
408
|
// to ask would be an operation an agent cannot reach.
|
|
@@ -44,7 +44,8 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
44
44
|
.map(async adapter => {
|
|
45
45
|
const entry = { adapterId: adapter.id, displayName: adapter.displayName,
|
|
46
46
|
present: false, version: null, versionOutput: null, installed: false,
|
|
47
|
-
diagnostics: [], capabilities: adapter.capabilities ?? {},
|
|
47
|
+
diagnostics: [], needsAction: [], capabilities: adapter.capabilities ?? {},
|
|
48
|
+
error: null };
|
|
48
49
|
|
|
49
50
|
try {
|
|
50
51
|
const output = await withTimeout(
|
|
@@ -65,6 +66,9 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
|
|
|
65
66
|
try {
|
|
66
67
|
const detected = await adapter.detect(context);
|
|
67
68
|
entry.diagnostics = detected.diagnostics ?? [];
|
|
69
|
+
// What a person has to do, as opposed to what is true. Adapters that
|
|
70
|
+
// have nothing to ask for say nothing.
|
|
71
|
+
entry.needsAction = detected.needsAction ?? [];
|
|
68
72
|
// "Installed" is the adapter's own answer, phrased in its own terms.
|
|
69
73
|
// The installer does not second-guess it by looking at files it does
|
|
70
74
|
// not understand.
|