agents-can-communicate 0.1.0 → 0.1.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/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/.claude-plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
- 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-kimi/plugin/.kimi-plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/config-merge.mjs +16 -1
- package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/json-text.mjs +195 -0
- package/node_modules/@agents-can-communicate/cli/package.json +1 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +4 -1
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +24 -3
- 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/plan.mjs +28 -5
- 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 +4 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-can-communicate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Coordinate this Codex session with other AI agent sessions working in the same workspace.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"keywords": ["coordination", "multi-agent", "claims", "handoff"],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-can-communicate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Coordinate this Kimi Code session with other AI agent sessions working in the same workspace: shared presence, resource claims, typed messages, and handoffs.",
|
|
5
5
|
"skills": "./skills/",
|
|
6
6
|
"sessionStart": {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
+
import { editJson } from "./json-text.mjs";
|
|
4
|
+
|
|
3
5
|
const MARKER = "acc:owned";
|
|
4
6
|
// Ownership of single entries inside a container someone else also writes to.
|
|
5
7
|
// `enabledPlugins` in a Claude Code settings file holds every plugin the user
|
|
@@ -150,10 +152,23 @@ export function jsonStyleOf(text) {
|
|
|
150
152
|
* Unchanged content is not rewritten at all, so a second install touches
|
|
151
153
|
* nothing. A file that does not exist yet is ACC's to create, and gets the
|
|
152
154
|
* conventional trailing newline.
|
|
155
|
+
*
|
|
156
|
+
* What is there is edited rather than re-emitted. Reading the style back out of
|
|
157
|
+
* the file kept the indentation, and still reformatted everything the style
|
|
158
|
+
* could not describe: a nested object a person had written on one line came
|
|
159
|
+
* back as three, and a blank line between sections was gone. The bytes are
|
|
160
|
+
* theirs, and the diff ACC leaves should be of what it changed.
|
|
153
161
|
*/
|
|
154
162
|
export async function writeForeignJson(file, value, { readFile, writeFile, mkdir }) {
|
|
155
163
|
const current = await readFile(file, "utf8").catch(() => null);
|
|
156
|
-
const
|
|
164
|
+
const style = jsonStyleOf(current);
|
|
165
|
+
// Null when the original cannot be read, or when the edit came back meaning
|
|
166
|
+
// something other than it was asked to write. Then this is the whole-file
|
|
167
|
+
// re-emit it has always been.
|
|
168
|
+
const spliced = typeof current === "string" ? editJson(current, value, style.indent) : null;
|
|
169
|
+
const text = spliced === null
|
|
170
|
+
? formatJsonAs(value, style)
|
|
171
|
+
: (style.trailingNewline === false ? spliced : `${spliced}\n`);
|
|
157
172
|
if (current === text) return false;
|
|
158
173
|
await mkdir(path.dirname(file), { recursive: true });
|
|
159
174
|
await writeFile(file, text);
|
|
@@ -8,6 +8,7 @@ export { assertRunner, bakeSkillCommand, defaultCli, defaultRunner, removeInstal
|
|
|
8
8
|
export { BEGIN, END, removeTomlBlock, renderBlock, stripBlock, tomlString, writeTomlBlock }
|
|
9
9
|
from "./toml-block.mjs";
|
|
10
10
|
export { projectContext } from "./context-projector.mjs";
|
|
11
|
+
export { editJson, readJson } from "./json-text.mjs";
|
|
11
12
|
export { formatJsonAs, jsonStyleOf, mergeOwnedConfig, mergeOwnedEntries, ownedEntries, ownedKeys,
|
|
12
13
|
acccreatedFile, removeIfEmpty, removeOwnedConfig, removeOwnedEntries, writeForeignJson,
|
|
13
14
|
blankJson, blankText } from "./config-merge.mjs";
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit a JSON document as text, so what was not changed is not rewritten.
|
|
3
|
+
*
|
|
4
|
+
* `JSON.parse` then `JSON.stringify` re-emits every byte of a file, which
|
|
5
|
+
* reformats the parts ACC was not asked to touch: a nested object a person kept
|
|
6
|
+
* on one line comes back as three, and a blank line between sections is gone.
|
|
7
|
+
* The bytes are the user's, and a tool that edits their settings should leave a
|
|
8
|
+
* diff of what it changed rather than of how it prints.
|
|
9
|
+
*
|
|
10
|
+
* So the original text is kept and spliced. Anything whose value is unchanged is
|
|
11
|
+
* copied across verbatim - not re-serialised and hoped to match - and only a
|
|
12
|
+
* member that was added, removed or replaced is written fresh.
|
|
13
|
+
*
|
|
14
|
+
* The result is parsed back and compared to the value it was asked to write. A
|
|
15
|
+
* splice that would change meaning is discarded rather than written, and the
|
|
16
|
+
* caller falls back to plain `JSON.stringify`: the worst this can do is what
|
|
17
|
+
* was already being done.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const WHITESPACE = " \t\n\r";
|
|
21
|
+
const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
22
|
+
|
|
23
|
+
const isObject = value =>
|
|
24
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read a JSON document into a tree that remembers where everything was.
|
|
28
|
+
*
|
|
29
|
+
* @returns the root node, or null for anything this cannot read - which the
|
|
30
|
+
* caller treats the same way as a file it must not touch.
|
|
31
|
+
*/
|
|
32
|
+
export function readJson(text) {
|
|
33
|
+
if (typeof text !== "string") return null;
|
|
34
|
+
let at = 0;
|
|
35
|
+
|
|
36
|
+
const skip = () => { while (at < text.length && WHITESPACE.includes(text[at])) at += 1; };
|
|
37
|
+
const fail = () => { throw new SyntaxError(`unreadable JSON at ${at}`); };
|
|
38
|
+
const expect = character => { if (text[at] !== character) fail(); at += 1; };
|
|
39
|
+
|
|
40
|
+
const readString = () => {
|
|
41
|
+
expect('"');
|
|
42
|
+
while (at < text.length) {
|
|
43
|
+
if (text[at] === "\\") { at += 2; continue; }
|
|
44
|
+
if (text[at] === '"') { at += 1; return; }
|
|
45
|
+
at += 1;
|
|
46
|
+
}
|
|
47
|
+
fail();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const readValue = () => {
|
|
51
|
+
skip();
|
|
52
|
+
const start = at;
|
|
53
|
+
if (text[at] === "{") return readObject(start);
|
|
54
|
+
if (text[at] === "[") return readArray(start);
|
|
55
|
+
if (text[at] === '"') { readString(); }
|
|
56
|
+
else {
|
|
57
|
+
// Numbers, true, false, null: read to the next structural character and
|
|
58
|
+
// let `JSON.parse` below be the judge of what was read.
|
|
59
|
+
while (at < text.length && !",}]".includes(text[at]) && !WHITESPACE.includes(text[at])) {
|
|
60
|
+
at += 1;
|
|
61
|
+
}
|
|
62
|
+
if (at === start) fail();
|
|
63
|
+
}
|
|
64
|
+
const slice = text.slice(start, at);
|
|
65
|
+
return { kind: "scalar", start, end: at, value: JSON.parse(slice) };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
function readObject(start) {
|
|
69
|
+
expect("{");
|
|
70
|
+
const members = [];
|
|
71
|
+
const value = {};
|
|
72
|
+
for (;;) {
|
|
73
|
+
// Everything between the previous member and this one - the comma, the
|
|
74
|
+
// newline, the indentation - is kept as it was written.
|
|
75
|
+
const gapStart = at;
|
|
76
|
+
skip();
|
|
77
|
+
if (text[at] === "}") { at += 1; break; }
|
|
78
|
+
if (members.length > 0) { expect(","); skip(); }
|
|
79
|
+
if (text[at] === "}") { at += 1; break; }
|
|
80
|
+
const keyStart = at;
|
|
81
|
+
readString();
|
|
82
|
+
const key = JSON.parse(text.slice(keyStart, at));
|
|
83
|
+
skip();
|
|
84
|
+
expect(":");
|
|
85
|
+
const node = readValue();
|
|
86
|
+
members.push({ key, gapStart, keyStart, valueStart: node.start, end: node.end, node });
|
|
87
|
+
// Defined rather than assigned: `value.__proto__ = …` sets the prototype
|
|
88
|
+
// of an ordinary object instead of adding a key to it, so a config with
|
|
89
|
+
// that key read back without it - and the comparison that decides whether
|
|
90
|
+
// a subtree changed would be answering about a different document than
|
|
91
|
+
// the one on disk. This is what `JSON.parse` does with the same input.
|
|
92
|
+
Object.defineProperty(value, key,
|
|
93
|
+
{ value: node.value, writable: true, enumerable: true, configurable: true });
|
|
94
|
+
}
|
|
95
|
+
return { kind: "object", start, end: at, value, members };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readArray(start) {
|
|
99
|
+
expect("[");
|
|
100
|
+
const items = [];
|
|
101
|
+
const value = [];
|
|
102
|
+
for (;;) {
|
|
103
|
+
skip();
|
|
104
|
+
if (text[at] === "]") { at += 1; break; }
|
|
105
|
+
if (items.length > 0) { expect(","); skip(); }
|
|
106
|
+
if (text[at] === "]") { at += 1; break; }
|
|
107
|
+
const node = readValue();
|
|
108
|
+
items.push(node);
|
|
109
|
+
value.push(node.value);
|
|
110
|
+
}
|
|
111
|
+
return { kind: "array", start, end: at, value, items };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const root = readValue();
|
|
116
|
+
skip();
|
|
117
|
+
// Trailing content means this is not the document it appears to be.
|
|
118
|
+
return at === text.length ? root : null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Write `value` into `text`, changing only what differs.
|
|
126
|
+
*
|
|
127
|
+
* @returns the edited document, or null when it cannot be done safely - an
|
|
128
|
+
* unreadable original, or a splice that came back meaning something else.
|
|
129
|
+
*/
|
|
130
|
+
export function editJson(text, value, indent) {
|
|
131
|
+
const root = readJson(text);
|
|
132
|
+
if (root === null) return null;
|
|
133
|
+
|
|
134
|
+
const unit = typeof indent === "number" ? " ".repeat(indent) : (indent ?? " ");
|
|
135
|
+
const pad = depth => unit.repeat(depth);
|
|
136
|
+
const broken = unit !== "";
|
|
137
|
+
|
|
138
|
+
// A value with no original to preserve: printed the way this file prints,
|
|
139
|
+
// then moved to the depth it sits at.
|
|
140
|
+
const fresh = (subject, depth) => {
|
|
141
|
+
const printed = JSON.stringify(subject, null, unit);
|
|
142
|
+
return broken ? printed.split("\n").join(`\n${pad(depth)}`) : printed;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const render = (node, subject, depth) => {
|
|
146
|
+
if (node !== null && same(node.value, subject)) return text.slice(node.start, node.end);
|
|
147
|
+
if (node?.kind === "object" && isObject(subject)) return spliceObject(node, subject, depth);
|
|
148
|
+
return fresh(subject, depth);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
function spliceObject(node, subject, depth) {
|
|
152
|
+
const byKey = new Map(node.members.map(member => [member.key, member]));
|
|
153
|
+
const pieces = [];
|
|
154
|
+
|
|
155
|
+
for (const key of Object.keys(subject)) {
|
|
156
|
+
const member = byKey.get(key);
|
|
157
|
+
if (member === undefined) {
|
|
158
|
+
// New: written in this file's own shape rather than copied from nowhere.
|
|
159
|
+
pieces.push(broken
|
|
160
|
+
? `\n${pad(depth + 1)}${JSON.stringify(key)}: ${fresh(subject[key], depth + 1)}`
|
|
161
|
+
: `${JSON.stringify(key)}:${fresh(subject[key], depth + 1)}`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
// The separator that preceded it, the key as it was spelled, and the
|
|
165
|
+
// colon and spacing after it: all of it is the user's, and none of it is
|
|
166
|
+
// what changed.
|
|
167
|
+
const separator = text.slice(member.gapStart, member.keyStart);
|
|
168
|
+
const label = text.slice(member.keyStart, member.valueStart);
|
|
169
|
+
pieces.push(separator.replace(",", "") + label + render(member.node, subject[key], depth + 1));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// The comma belongs between members, so it is placed rather than copied:
|
|
173
|
+
// a member that used to be first no longer is, and one that is now first
|
|
174
|
+
// must not arrive with the comma it used to carry.
|
|
175
|
+
const body = pieces.join(",");
|
|
176
|
+
// What sat between the last member and the brace, when the last member is
|
|
177
|
+
// still the one that was there; otherwise this file's own closing shape.
|
|
178
|
+
const last = node.members.at(-1);
|
|
179
|
+
const closing = last !== undefined && Object.keys(subject).at(-1) === last.key
|
|
180
|
+
? text.slice(last.end, node.end - 1)
|
|
181
|
+
: (broken ? `\n${pad(depth)}` : "");
|
|
182
|
+
if (pieces.length === 0) return "{}";
|
|
183
|
+
return `{${body}${closing}}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const edited = render(root, value, 0);
|
|
187
|
+
// The safety net. A splice that parses to something else is not written.
|
|
188
|
+
let read;
|
|
189
|
+
try {
|
|
190
|
+
read = JSON.parse(edited);
|
|
191
|
+
} catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return same(read, value) ? edited : null;
|
|
195
|
+
}
|
|
@@ -57,7 +57,10 @@ export const COMMANDS = Object.freeze({
|
|
|
57
57
|
config: { required: [], optional: [], flags: ["yes", "force"],
|
|
58
58
|
subcommands: ["init", "validate"] },
|
|
59
59
|
install: { required: [], optional: ["adapter", "home"], flags: ["dry-run", "yes"] },
|
|
60
|
-
|
|
60
|
+
// `--dry-run` on both, because the preview was computed for either action and
|
|
61
|
+
// only `install` could ask for it. Removal is the side that reaches into a
|
|
62
|
+
// client's configuration - including a client that has left the machine.
|
|
63
|
+
uninstall: { required: [], optional: ["adapter", "home"], flags: ["dry-run", "yes"] },
|
|
61
64
|
// The two things a person types first after installing from a registry. The
|
|
62
65
|
// CLI answered neither: `acc --version` and `acc --help` were both "unknown
|
|
63
66
|
// command", and `acc` on its own asked for a command without naming one.
|
|
@@ -5,7 +5,7 @@ import { createClaudeCodeAdapter } from "@agents-can-communicate/adapter-claude-
|
|
|
5
5
|
import { createCodexAdapter } from "@agents-can-communicate/adapter-codex";
|
|
6
6
|
import { createGeminiCliAdapter } from "@agents-can-communicate/adapter-gemini-cli";
|
|
7
7
|
import { createKimiAdapter } from "@agents-can-communicate/adapter-kimi";
|
|
8
|
-
import { applyPlan, detectInstallation, planInstallation }
|
|
8
|
+
import { applyPlan, detectInstallation, loadOwnership, planInstallation }
|
|
9
9
|
from "@agents-can-communicate/installer";
|
|
10
10
|
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
11
11
|
|
|
@@ -80,6 +80,21 @@ export function failureOf({ action, acted, failed = [] }) {
|
|
|
80
80
|
describeOutcome({ action, acted, failed }), { failed });
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* How many adapters this actually did something to.
|
|
85
|
+
*
|
|
86
|
+
* `applied` means the adapter's own step ran, which on an uninstall is true of
|
|
87
|
+
* every client on the machine whether ACC had ever written to it or not. So
|
|
88
|
+
* `uninstalled 3 adapter(s)` was the report on a machine where ACC had installed
|
|
89
|
+
* nothing at all - printed by the same run that skipped the one client it had
|
|
90
|
+
* actually written to.
|
|
91
|
+
*/
|
|
92
|
+
export function actedOn(result) {
|
|
93
|
+
return result.operations.filter(operation => operation.applied
|
|
94
|
+
&& (result.action === "install"
|
|
95
|
+
|| (operation.removed?.length ?? 0) + (operation.changes?.length ?? 0) > 0)).length;
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
export async function runInstallCommand({ options, runtime, action = "install" }) {
|
|
84
99
|
const adapters = selectAdapters(options.adapter);
|
|
85
100
|
const home = options.home ?? runtime.env?.HOME ?? homedir();
|
|
@@ -88,12 +103,18 @@ export async function runInstallCommand({ options, runtime, action = "install" }
|
|
|
88
103
|
env: runtime.env ?? {} });
|
|
89
104
|
|
|
90
105
|
const detected = await detectInstallation({ adapters, context });
|
|
91
|
-
|
|
106
|
+
// An uninstall is planned from what ACC recorded writing, not only from what
|
|
107
|
+
// is on the machine now. A client can be removed after ACC installed into it,
|
|
108
|
+
// and its configuration directory - with ACC's files in it - stays behind.
|
|
109
|
+
const recorded = action === "uninstall"
|
|
110
|
+
? (await loadOwnership({ dataHome })).installs
|
|
111
|
+
: [];
|
|
112
|
+
const plan = planInstallation({ adapters, detected, context, action, recorded });
|
|
92
113
|
|
|
93
114
|
const dryRun = options.dryRun === true;
|
|
94
115
|
const result = await applyPlan({ plan, adapters, context, dataHome, dryRun });
|
|
95
116
|
|
|
96
|
-
const acted = result
|
|
117
|
+
const acted = actedOn(result);
|
|
97
118
|
if (dryRun) {
|
|
98
119
|
return { data: { ...result, plan, dataHome },
|
|
99
120
|
text: [`would ${action}:`, ...plan.operations.flatMap(operation => operation.summary),
|
|
@@ -7,11 +7,16 @@ import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
|
7
7
|
* makes `--dry-run` worth reading - a plan computed differently from the thing
|
|
8
8
|
* it previews is a decoration, and the operator would find out only afterwards.
|
|
9
9
|
*/
|
|
10
|
-
export function planInstallation({ adapters, detected, context, action = "install"
|
|
10
|
+
export function planInstallation({ adapters, detected, context, action = "install",
|
|
11
|
+
recorded = [] }) {
|
|
11
12
|
if (!["install", "uninstall"].includes(action)) {
|
|
12
13
|
throw new AccError(EXIT.USAGE, `unknown installation action: ${action}`, { action });
|
|
13
14
|
}
|
|
14
15
|
const byId = new Map(adapters.map(adapter => [adapter.id, adapter]));
|
|
16
|
+
// What ACC recorded writing, by client. For an uninstall this is the
|
|
17
|
+
// authority rather than detection: the record is the only account of what was
|
|
18
|
+
// written, and a client's configuration directory outlives the client.
|
|
19
|
+
const recordedById = new Map(recorded.map(install => [install.adapterId, install]));
|
|
15
20
|
const operations = [];
|
|
16
21
|
const skipped = [];
|
|
17
22
|
|
|
@@ -24,20 +29,32 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
24
29
|
skipped.push({ adapterId: entry.adapterId, reason: "no adapter for this client" });
|
|
25
30
|
continue;
|
|
26
31
|
}
|
|
27
|
-
|
|
32
|
+
// A client that has left the machine is still worth visiting when ACC wrote
|
|
33
|
+
// to it. Skipping it made that install permanently unremovable: `acc
|
|
34
|
+
// uninstall` reported success and exit 0, left ACC's tree in the client's
|
|
35
|
+
// configuration directory and ACC's entries in the user's own settings
|
|
36
|
+
// file, and said the same thing on every run afterwards.
|
|
37
|
+
const record = action === "uninstall" && !entry.present
|
|
38
|
+
? recordedById.get(entry.adapterId)
|
|
39
|
+
: undefined;
|
|
40
|
+
|
|
41
|
+
if (!entry.present && record === undefined) {
|
|
28
42
|
// Named rather than dropped: "nothing happened" and "that client is not
|
|
29
43
|
// installed on this machine" look the same in an empty list.
|
|
30
44
|
skipped.push({ adapterId: entry.adapterId,
|
|
31
45
|
reason: `${entry.displayName ?? entry.adapterId} is not installed on this machine` });
|
|
32
46
|
continue;
|
|
33
47
|
}
|
|
34
|
-
if (typeof adapter.planInstall !== "function") {
|
|
48
|
+
if (record === undefined && typeof adapter.planInstall !== "function") {
|
|
35
49
|
skipped.push({ adapterId: entry.adapterId,
|
|
36
50
|
reason: "this adapter cannot describe what it would write" });
|
|
37
51
|
continue;
|
|
38
52
|
}
|
|
39
53
|
|
|
40
|
-
|
|
54
|
+
// From the record when the client is gone, because that is what was written
|
|
55
|
+
// and so what will be removed. Asking the adapter instead would describe an
|
|
56
|
+
// install for a machine this one no longer is.
|
|
57
|
+
const artifacts = (record?.artifacts ?? adapter.planInstall(context))
|
|
41
58
|
.map(artifact => ({ path: artifact.path, kind: artifact.kind ?? "file" }))
|
|
42
59
|
.sort((a, b) => a.path.localeCompare(b.path));
|
|
43
60
|
|
|
@@ -45,12 +62,18 @@ export function planInstallation({ adapters, detected, context, action = "instal
|
|
|
45
62
|
adapterId: adapter.id,
|
|
46
63
|
displayName: adapter.displayName,
|
|
47
64
|
action,
|
|
48
|
-
clientVersion: entry.version ?? null,
|
|
65
|
+
clientVersion: entry.version ?? record?.version ?? null,
|
|
66
|
+
// "Remove these files for a client that is not here" is a different thing
|
|
67
|
+
// to approve than an ordinary uninstall, so it is said rather than left
|
|
68
|
+
// to be inferred from a client version that is null.
|
|
69
|
+
clientPresent: entry.present === true,
|
|
49
70
|
alreadyInstalled: entry.installed === true,
|
|
50
71
|
artifacts,
|
|
51
72
|
// Said in the operator's terms, not in paths: which files ACC creates
|
|
52
73
|
// outright and which belong to the user and are only edited.
|
|
53
74
|
summary: [
|
|
75
|
+
...(entry.present ? [] : [`${adapter.displayName ?? adapter.id} is no longer on `
|
|
76
|
+
+ "this machine; removing what ACC recorded writing"]),
|
|
54
77
|
...artifacts.filter(a => a.kind === "tree")
|
|
55
78
|
.map(a => `${action === "install" ? "create" : "remove"} ${a.path}`),
|
|
56
79
|
...artifacts.filter(a => a.kind === "merge")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agents-can-communicate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Model- and harness-agnostic coordination for independent AI agent sessions.",
|
|
6
6
|
"keywords": [
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"node": ">=24"
|
|
37
37
|
},
|
|
38
38
|
"bin": {
|
|
39
|
-
"acc": "
|
|
40
|
-
"acc-mcp": "
|
|
41
|
-
"acc-hook": "
|
|
39
|
+
"acc": "bin/acc.mjs",
|
|
40
|
+
"acc-mcp": "bin/acc-mcp.mjs",
|
|
41
|
+
"acc-hook": "bin/acc-hook.mjs"
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
44
|
"bin/",
|