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.
Files changed (89) hide show
  1. package/README.md +29 -21
  2. package/bin/acc-indicator.mjs +5 -0
  3. package/docs/CAPABILITIES.md +3 -0
  4. package/docs/CLI.md +34 -7
  5. package/docs/MCP.md +42 -2
  6. package/docs/PROTOCOL.md +19 -0
  7. package/docs/STATUS_INDICATOR.md +128 -0
  8. package/docs/TROUBLESHOOTING.md +10 -0
  9. package/docs/UPGRADING.md +16 -0
  10. package/docs/WHY_ACC.md +14 -8
  11. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
  12. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  13. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +29 -5
  14. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +3 -0
  15. package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-install.mjs +80 -0
  16. package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-render.mjs +8 -0
  17. package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +7 -4
  18. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  19. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/hooks/indicator.mjs +67 -0
  20. package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +29 -5
  21. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +1 -0
  22. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +16 -5
  23. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  24. package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +48 -5
  25. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +9 -6
  26. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +16 -5
  27. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  28. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/skills/acc/SKILL.md +29 -5
  29. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  30. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  31. package/node_modules/@agents-can-communicate/adapter-grok/plugin/skills/acc/SKILL.md +29 -5
  32. package/node_modules/@agents-can-communicate/adapter-grok/src/adapter.mjs +4 -0
  33. package/node_modules/@agents-can-communicate/adapter-grok/src/indicator-install.mjs +30 -0
  34. package/node_modules/@agents-can-communicate/adapter-grok/src/indicator.mjs +23 -0
  35. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +9 -5
  36. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  37. package/node_modules/@agents-can-communicate/adapter-kimi/plugin/skills/acc/SKILL.md +29 -5
  38. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +4 -0
  39. package/node_modules/@agents-can-communicate/adapter-kimi/src/indicator-install.mjs +30 -0
  40. package/node_modules/@agents-can-communicate/adapter-kimi/src/indicator.mjs +17 -0
  41. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +12 -6
  42. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  43. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +1 -0
  44. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +5 -1
  45. package/node_modules/@agents-can-communicate/adapter-sdk/src/indicator-command.mjs +14 -0
  46. package/node_modules/@agents-can-communicate/adapter-sdk/src/indicator-text.mjs +13 -0
  47. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +2 -0
  48. package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation-order.mjs +12 -0
  49. package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation-writer.mjs +37 -0
  50. package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation.mjs +47 -0
  51. package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-indicator.mjs +141 -0
  52. package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-scan.mjs +99 -0
  53. package/node_modules/@agents-can-communicate/cli/package.json +2 -1
  54. package/node_modules/@agents-can-communicate/cli/src/args.mjs +5 -3
  55. package/node_modules/@agents-can-communicate/cli/src/claim-command.mjs +64 -0
  56. package/node_modules/@agents-can-communicate/cli/src/help.mjs +15 -8
  57. package/node_modules/@agents-can-communicate/cli/src/index.mjs +1 -0
  58. package/node_modules/@agents-can-communicate/cli/src/indicator-command.mjs +65 -0
  59. package/node_modules/@agents-can-communicate/cli/src/indicator-previous-command.mjs +84 -0
  60. package/node_modules/@agents-can-communicate/cli/src/indicator-previous-windows.mjs +8 -0
  61. package/node_modules/@agents-can-communicate/cli/src/indicator.mjs +112 -0
  62. package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +4 -0
  63. package/node_modules/@agents-can-communicate/cli/src/main.mjs +8 -54
  64. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +19 -0
  65. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +5 -3
  66. package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +2 -0
  67. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  68. package/node_modules/@agents-can-communicate/core/src/claim-selectors.mjs +62 -0
  69. package/node_modules/@agents-can-communicate/core/src/claims.mjs +88 -81
  70. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  71. package/node_modules/@agents-can-communicate/delivery-router/src/refresh-binding.mjs +9 -6
  72. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +19 -5
  73. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  74. package/node_modules/@agents-can-communicate/installer/package.json +1 -1
  75. package/node_modules/@agents-can-communicate/installer/src/apply.mjs +3 -0
  76. package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +1 -0
  77. package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +2 -1
  78. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +11 -2
  79. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  80. package/node_modules/@agents-can-communicate/mcp-server/src/claim-tools.mjs +56 -0
  81. package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +9 -0
  82. package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +7 -8
  83. package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +27 -9
  84. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  85. package/node_modules/@agents-can-communicate/protocol/src/index.mjs +1 -0
  86. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  87. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -1
  88. package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +13 -0
  89. package/package.json +3 -1
@@ -1,3 +1,5 @@
1
+ import { preflightGrokIndicator } from "./indicator-install.mjs";
2
+ import { grokIndicator } from "./indicator.mjs";
1
3
  import { defineAdapter, projectContext, projectContextResult }
2
4
  from "@agents-can-communicate/adapter-sdk";
3
5
  import certification from "../certification.json" with { type: "json" };
@@ -22,6 +24,7 @@ export const GROK_VERSION = "1.0.13";
22
24
  export function createGrokAdapter() {
23
25
  return defineAdapter({
24
26
  id: "grok",
27
+ statusIndicator: grokIndicator,
25
28
  displayName: "Grok",
26
29
  // Native Mach-O at ~/.grok/bin/grok. `ps -o comm=` on 1.0.13 reports `grok`.
27
30
  client: { command: "grok", certificationName: "grok", versionArgs: ["--version"] },
@@ -41,6 +44,7 @@ export function createGrokAdapter() {
41
44
  planInstall: context => planGrokInstall(forClient(context)),
42
45
  detect: context => detectGrok(forClient(context)),
43
46
  install: context => installGrokHooks(forClient(context)),
47
+ preflightUninstall: context => preflightGrokIndicator(forClient(context)),
44
48
  uninstall: context => uninstallGrokHooks(forClient(context)),
45
49
 
46
50
  doctor: async context => {
@@ -0,0 +1,30 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from 'node:path';
3
+ import {configureTomlIndicator,indicatorCommand,compareVersionOrder,versionOrder} from '@agents-can-communicate/adapter-sdk';
4
+ import {grokIndicator} from './indicator.mjs';
5
+
6
+ export const grokIndicatorPaths = context => {
7
+ const root=context.grokHome??path.join(context.home,'.grok');
8
+ return {file:path.join(root,'config.toml'),marker:path.join(context.dataHome??root,'acc','adapter-grok','indicator.json'),
9
+ shim:path.join(root,'hooks','acc-indicator.mjs')};
10
+ };
11
+ export async function configureGrokIndicator(context,enabled) {
12
+ const paths=grokIndicatorPaths(context);
13
+
14
+ if(enabled&&!supportsGrokIndicator(context))return [];
15
+ return configureTomlIndicator({...paths,table:['ui','status_line'],enabled,set:async()=>({type:'command',command:enabled?await indicatorCommand({context,adapterId:'grok',...paths,shell:'portable'}):''}),defaults:{refresh_interval:1}});
16
+ }
17
+
18
+ export const preflightGrokIndicator = context => configureTomlIndicator({...grokIndicatorPaths(context),table:['ui','status_line'],enabled:false,dryRun:true});
19
+
20
+ function supportsGrokIndicator(context) {
21
+ return typeof context.clientVersion === "string"
22
+ && compareVersionOrder(versionOrder(context.clientVersion), versionOrder(grokIndicator.minimumVersion)) >= 0;
23
+ }
24
+
25
+ export function planGrokIndicator(context) {
26
+ const paths = grokIndicatorPaths(context);
27
+ if (context.indicator === "on" ? !supportsGrokIndicator(context) : !existsSync(paths.marker)) return [];
28
+ return [{path: paths.file, kind: "merge"},
29
+ ...(context.indicator === "on" && context.hostPlatform === "win32" ? [{path: paths.shim, kind: "tree"}] : [])];
30
+ }
@@ -0,0 +1,23 @@
1
+ import {indicatorGlyph,footerText} from '@agents-can-communicate/adapter-sdk';
2
+
3
+ export const grokIndicator = {
4
+ minimumVersion:'1.0.46', reload:'start a new Grok session',
5
+ nativeSessionId: payload=>payload.session_id,
6
+ renderText:indicatorGlyph,
7
+ composeText({payload={},indicator,previous='',settings={}}) {
8
+ const cwd=footerText(payload.workspace?.current_dir??payload.cwd).split(/[\\/]/).filter(Boolean).at(-1);
9
+ const percent=payload.context_window?.used_percentage;
10
+ const cost=payload.cost?.total_cost_usd;
11
+ const started=payload.turn?.started_at_ms;
12
+ const elapsed=Number.isFinite(started)?Date.now()-started:NaN;
13
+ const slots={cwd,model:footerText(payload.model?.display_name??payload.model?.id),
14
+ context:Number.isFinite(percent)?`${Math.round(percent)}% ctx`:'',
15
+ cost:Number.isFinite(cost)&&cost>=0.005?`$${cost.toFixed(2)}`:'',
16
+ 'turn-timer':Number.isFinite(elapsed)&&elapsed>=1000?`${Math.floor(elapsed/1000)}s`:'',
17
+ 'session-name':footerText(payload.session_name)};
18
+ const items=Array.isArray(settings.items)?settings.items:['cwd','model','context'];
19
+ const base=previous.trimEnd() || items.map(id=>slots[id]).filter(Boolean).join(' │ ');
20
+ const [first,...rest]=base.split(/\r?\n/);
21
+ return [[indicator,first].filter(Boolean).join(' │ '),...rest].join('\n');
22
+ },
23
+ };
@@ -1,3 +1,4 @@
1
+ import {configureGrokIndicator, planGrokIndicator} from "./indicator-install.mjs";
1
2
  import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { fileURLToPath } from "node:url";
@@ -64,9 +65,10 @@ const withShim = (wiring, shim, windowsNode = null) => ({
64
65
  }))])),
65
66
  });
66
67
 
67
- export async function installGrokHooks({ grokHome, home, runner, cli, node,
68
- hostPlatform = process.platform }) {
68
+ export async function installGrokHooks(context) {
69
+ const {grokHome, home, runner, cli, node, hostPlatform = process.platform} = context;
69
70
  const root = grokHome ?? grokHomeOf({ home, grokHome });
71
+ const indicatorChanges = await configureGrokIndicator(context, context.indicator === "on");
70
72
  const template = await readJson(path.join(bundle, "hooks", "hooks.json"), { hooks: {} });
71
73
  const shim = await writeHookShim({ dir: path.join(root, "hooks"), adapterId: "grok",
72
74
  runner, node, name: SHIM_NAME, platform: hostPlatform });
@@ -80,12 +82,13 @@ export async function installGrokHooks({ grokHome, home, runner, cli, node,
80
82
 
81
83
  await writeJson(hooksFile(root), withShim(template, shim,
82
84
  hostPlatform === "win32" ? node ?? process.execPath : null));
83
- return { ok: true, changes: [hooksFile(root), shim, skills], diagnostics: [] };
85
+ return { ok: true, changes: [hooksFile(root), shim, skills, ...indicatorChanges], diagnostics: [] };
84
86
  }
85
87
 
86
- export async function uninstallGrokHooks({ grokHome, home, keep = [], hostPlatform }) {
88
+ export async function uninstallGrokHooks(context) {
89
+ const {grokHome, home, keep = [], hostPlatform} = context;
87
90
  const root = grokHome ?? grokHomeOf({ home, grokHome });
88
- const changes = [];
91
+ const changes = await configureGrokIndicator(context, false);
89
92
  for (const target of [hooksFile(root), shimPath(root, hostPlatform), skillPath(root)]) {
90
93
  if (await removeInstalledTree(target, keep)) changes.push(target);
91
94
  }
@@ -106,6 +109,7 @@ export async function detectGrok({ grokHome, home }) {
106
109
  export function planGrokInstall(context) {
107
110
  const root = grokHomeOf(context);
108
111
  return [
112
+ ...planGrokIndicator(context),
109
113
  { path: hooksFile(root), kind: "tree" },
110
114
  { path: shimPath(root, context.hostPlatform), kind: "tree" },
111
115
  { path: skillPath(root), kind: "tree" },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-kimi",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -57,19 +57,43 @@ Do this once, not every turn. Update it only when the scope or mode materially
57
57
  changes. `--hint` is important: it lets ACC match your plan against a peer's
58
58
  claim. Intent is awareness, not permission.
59
59
 
60
- Before changing shared files, claim the smallest useful resource:
60
+ Before changing shared files, claim the smallest useful resources in one operation:
61
61
 
62
62
  ```bash
63
- {{ACC}} claim --resource 'file:packages/core/**' --reason "porting the store"
63
+ {{ACC}} claim --resource 'file:packages/core/**' \
64
+ --resource 'file:packages/protocol/**' --reason "porting the claim model"
64
65
  ```
65
66
 
66
- Exit 5 means a conflict. Do not work around it silently. Narrow your scope,
67
- contact the owner, or ask the human. Give a claim back explicitly when useful:
67
+ Repeat `--resource` for an atomic batch, or supply it once for one claim.
68
+ Shared mode, enforcement, reason, and lease apply to every resource. Duplicate resources after
69
+ normalization are rejected. Exit 5 means a conflict: no claim is created or
70
+ renewed. Batch `details.conflicts` names blocked resources and their holders.
71
+ Narrow your scope, contact the owner, or ask the human. Repeating a successful
72
+ claim command renews your existing claims on those resources.
73
+
74
+ Release selected claims when useful:
68
75
 
69
76
  ```bash
70
- {{ACC}} release --resource 'file:packages/core/**'
77
+ {{ACC}} release --resource 'file:packages/core/**' \
78
+ --resource 'file:packages/protocol/**'
71
79
  ```
72
80
 
81
+ Choose exactly one selector family: repeated `--resource`, repeated `--claim`,
82
+ or `--all`. Duplicate selectors after normalization are rejected. A missing or
83
+ foreign explicit target fails without releasing any claims. Repeating an explicit release after its targets were removed is an error.
84
+ Batch release accepts no force-release authority.
85
+
86
+ To release all your claims while keeping your session and intent open:
87
+
88
+ ```bash
89
+ {{ACC}} release --all
90
+ ```
91
+
92
+ This selects only your session's current claims in its workspace. An empty set
93
+ succeeds. Each retry selects the current set and can release newly acquired claims.
94
+ Use `work --clear` to clear intent, `ack` to acknowledge a message, and `finish`
95
+ to record a handoff and close your ACC presence.
96
+
73
97
  ## Communicate only when it changes another agent's work
74
98
 
75
99
  Send a message for a dependency, conflict, direct question, decision, or
@@ -1,3 +1,5 @@
1
+ import { preflightKimiIndicator } from "./indicator-install.mjs";
2
+ import { kimiIndicator } from "./indicator.mjs";
1
3
  import { defineAdapter, projectContext, projectContextResult }
2
4
  from "@agents-can-communicate/adapter-sdk";
3
5
  import certification from "../certification.json" with { type: "json" };
@@ -28,6 +30,7 @@ export const KIMI_CODE_VERSION = "0.36.1";
28
30
  export function createKimiAdapter() {
29
31
  return defineAdapter({
30
32
  id: "kimi",
33
+ statusIndicator: kimiIndicator,
31
34
  displayName: "Kimi Code",
32
35
  // The binary this client actually installs. Probed for a version to
33
36
  // decide whether the client is on this machine, so it has to be the
@@ -57,6 +60,7 @@ export function createKimiAdapter() {
57
60
  planInstall: context => planKimiInstall(forClient(context)),
58
61
  detect: context => detectKimi(forClient(context)),
59
62
  install: context => installKimiPlugin(forClient(context)),
63
+ preflightUninstall: context => preflightKimiIndicator(forClient(context)),
60
64
  uninstall: context => uninstallKimiPlugin(forClient(context)),
61
65
 
62
66
  doctor: async context => {
@@ -0,0 +1,30 @@
1
+ import { existsSync } from "node:fs";
2
+ import path from 'node:path';
3
+ import {configureTomlIndicator,indicatorCommand,compareVersionOrder,versionOrder} from '@agents-can-communicate/adapter-sdk';
4
+ import {kimiIndicator} from './indicator.mjs';
5
+
6
+ export const kimiIndicatorPaths = context => ({
7
+ file:path.join(context.home,'tui.toml'),
8
+ marker:path.join(context.dataHome??context.home,'acc','adapter-kimi','indicator.json'),
9
+ shim:path.join(context.home,'acc','indicator.mjs'),
10
+ });
11
+ export async function configureKimiIndicator(context,enabled) {
12
+ const paths=kimiIndicatorPaths(context);
13
+
14
+ if(enabled&&!supportsKimiIndicator(context))return [];
15
+ return configureTomlIndicator({...paths,table:['status_line'],enabled,set:async()=>({command:enabled?await indicatorCommand({context,adapterId:'kimi',...paths,shell:'cmd'}):''})});
16
+ }
17
+
18
+ export const preflightKimiIndicator = context => configureTomlIndicator({...kimiIndicatorPaths(context),table:['status_line'],enabled:false,dryRun:true});
19
+
20
+ function supportsKimiIndicator(context) {
21
+ return typeof context.clientVersion === "string"
22
+ && compareVersionOrder(versionOrder(context.clientVersion), versionOrder(kimiIndicator.minimumVersion)) >= 0;
23
+ }
24
+
25
+ export function planKimiIndicator(context) {
26
+ const paths = kimiIndicatorPaths(context);
27
+ if (context.indicator === "on" ? !supportsKimiIndicator(context) : !existsSync(paths.marker)) return [];
28
+ return [{path: paths.file, kind: "merge"},
29
+ ...(context.indicator === "on" && context.hostPlatform === "win32" ? [{path: paths.shim, kind: "tree"}] : [])];
30
+ }
@@ -0,0 +1,17 @@
1
+ import {indicatorGlyph,footerText,footerCwd} from '@agents-can-communicate/adapter-sdk';
2
+
3
+ export const kimiIndicator = {
4
+ minimumVersion:'2.1.1', reload:'run /reload-tui', budgetMs:180, previousTimeoutMs:80,
5
+ nativeSessionId: payload=>payload.sessionId,
6
+ renderText:indicatorGlyph,
7
+ composeText({payload={},indicator,previous='',settings={}}) {
8
+ const mode=[payload.permissionMode==='auto'?'auto':payload.permissionMode==='yolo'?'yolo':'',
9
+ payload.planMode===true?'plan':''].filter(Boolean).join(' · ');
10
+ const slots={mode,model:footerText(payload.model),cwd:footerCwd(payload.cwd),git:footerText(payload.gitBranch)};
11
+ const items=Array.isArray(settings.items)?settings.items:['mode','model','cwd','git'];
12
+ const base=previous.trimEnd().split(/\r?\n/)[0] || items.map(id=>slots[id]).filter(Boolean).join(' · ');
13
+ // The client has no session until the first prompt. This is not an ACC failure.
14
+ if(payload.sessionId==='')return base;
15
+ return [indicator,base].filter(Boolean).join(' │ ');
16
+ },
17
+ };
@@ -1,3 +1,4 @@
1
+ import {configureKimiIndicator, planKimiIndicator} from "./indicator-install.mjs";
1
2
  import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
 
@@ -125,8 +126,8 @@ const registerPlugin = (registry, root) => {
125
126
  plugins: [...plugins, { id: PLUGIN_NAME, root, source: "local", enabled: true }] };
126
127
  };
127
128
 
128
- export async function installKimiPlugin({ home, runner = defaultRunner(), node, cli,
129
- hostPlatform = process.platform }) {
129
+ export async function installKimiPlugin(context) {
130
+ const {home, runner = defaultRunner(), node, cli, hostPlatform = process.platform} = context;
130
131
  // A hook whose command does not exist fails silently, on every event, for as
131
132
  // long as it stays installed: the client reports nothing and ACC simply never
132
133
  // sees a session. Writing that entry and hoping is worse than refusing.
@@ -135,6 +136,7 @@ export async function installKimiPlugin({ home, runner = defaultRunner(), node,
135
136
  // plugin tree is laid down that nothing will then be able to remove.
136
137
  const registered = await readJson(registryPath(home), { version: 1, plugins: [] });
137
138
 
139
+ const indicatorChanges = await configureKimiIndicator(context, context.indicator === "on");
138
140
  const target = pluginPath(home);
139
141
  await rm(target, { recursive: true, force: true });
140
142
  await cp(bundle, target, { recursive: true });
@@ -161,13 +163,15 @@ export async function installKimiPlugin({ home, runner = defaultRunner(), node,
161
163
  await writeForeignJson(registry, registerPlugin(registered, target),
162
164
  { readFile, writeFile, mkdir });
163
165
 
164
- return { ok: true, changes: [target, file, registry], diagnostics: [] };
166
+ return { ok: true, changes: [target, file, registry, ...indicatorChanges], diagnostics: context.indicator === "on"
167
+ ? ["Kimi custom status line retains model, mode, cwd and branch; native context stays below. Goal and task badges are unavailable in the status payload."] : [] };
165
168
  }
166
169
 
167
- export async function uninstallKimiPlugin({ home, keep = [] }) {
170
+ export async function uninstallKimiPlugin(context) {
171
+ const {home, keep = []} = context;
168
172
  const file = configPath(home);
169
173
  const existing = await readText(file, null);
170
- const changes = [];
174
+ const changes = await configureKimiIndicator(context, false);
171
175
  if (existing !== null) {
172
176
  const stripped = stripBlock(existing);
173
177
  if (stripped !== existing) changes.push(file);
@@ -218,8 +222,10 @@ export async function detectKimi({ home, runner = defaultRunner() }) {
218
222
  * The paths an install would write, without writing them. Same helpers as the
219
223
  * install, so a dry run cannot drift from what actually happens.
220
224
  */
221
- export function planKimiInstall({ home }) {
225
+ export function planKimiInstall(context) {
226
+ const {home} = context;
222
227
  return [
228
+ ...planKimiIndicator(context),
223
229
  { path: pluginPath(home), kind: "tree" },
224
230
  { path: configPath(home), kind: "merge" },
225
231
  { path: registryPath(home), kind: "merge" },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/adapter-sdk",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -58,6 +58,7 @@ const ownBinary = name => {
58
58
  };
59
59
 
60
60
  export const defaultRunner = () => ownBinary("acc-hook.mjs");
61
+ export const defaultIndicator = () => ownBinary("acc-indicator.mjs");
61
62
  export const defaultAntigravityRelay = () => ownBinary("acc-antigravity-relay.mjs");
62
63
 
63
64
  export const runnerExists = async runner => {
@@ -11,7 +11,7 @@ export { NATIVE_ACTIVATION_KINDS, NATIVE_BINDING_MODES, NATIVE_REASON_CODES,
11
11
  from "./native-delivery.mjs";
12
12
  export { EVENT_KINDS, NORMALIZED_EVENT_KEYS, normalizedEvent } from "./events.mjs";
13
13
  export { assertRunner, bakeSkillCommand, defaultAntigravityRelay, defaultCli, isShellWord,
14
- defaultRunner, removeInstalledTree, runnerExists, shellQuote, writeCliShim, writeHookShim, writeNodeShim }
14
+ defaultRunner, defaultIndicator, removeInstalledTree, runnerExists, shellQuote, writeCliShim, writeHookShim, writeNodeShim }
15
15
  from "./hook-shim.mjs";
16
16
  export { BEGIN, END, removeTomlBlock, renderBlock, stripBlock, tomlString, writeTomlBlock }
17
17
  from "./toml-block.mjs";
@@ -28,6 +28,7 @@ export { clearSessionBinding, listSessionBindings, loadSessionBinding,
28
28
  from "./session-binding.mjs";
29
29
 
30
30
  export { clearNativeAttempt, loadNativeAttempt, storeNativeAttempt } from "./native-attempt.mjs";
31
+ export { OFFER_ERRORS, loadOfferObservation, storeOfferObservation } from "./offer-observation.mjs";
31
32
  export { channelSocketDirectory } from "./channel-directory.mjs";
32
33
  export { processIsZombie, readProcessArgs, readProcessTable, readWindowsProcess, splitWindowsCommandLine }
33
34
  from "./process-table.mjs";
@@ -37,3 +38,6 @@ export { windowsHookCommand } from "./windows-command.mjs";
37
38
  export { shortPath } from "./windows-command.mjs";
38
39
  export { mergeEnv } from "./executables.mjs";
39
40
  export { closedTo, isWindowsPlatform, openRegularNoFollow } from "./private-files.mjs";
41
+ export { configureTomlIndicator, readTomlIndicatorSettings } from './toml-indicator.mjs';
42
+ export { indicatorGlyph, footerText, footerCwd } from './indicator-text.mjs';
43
+ export { indicatorCommand } from './indicator-command.mjs';
@@ -0,0 +1,14 @@
1
+ import {mkdir,writeFile} from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {defaultIndicator,shellQuote} from './hook-shim.mjs';
4
+ import {windowsHookCommand} from './windows-command.mjs';
5
+
6
+ export async function indicatorCommand({context,adapterId,marker,shim,shell}) {
7
+ const node=context.node??process.execPath,reader=context.indicatorRunner??defaultIndicator();
8
+ const args=['--adapter',adapterId,'--previous',marker];
9
+ if((context.hostPlatform??process.platform)!=='win32')return [node,reader,...args].map(shellQuote).join(' ');
10
+ // Bake paths as JS data, so no shell expands the marker path or its arguments.
11
+ await mkdir(path.dirname(shim),{recursive:true});
12
+ await writeFile(shim,`import {pathToFileURL} from 'node:url';\nprocess.argv = [process.execPath,${JSON.stringify(reader)},...${JSON.stringify(args)}];\nawait import(pathToFileURL(${JSON.stringify(reader)}).href);\n`);
13
+ return windowsHookCommand(shell,{node,shim,args:[]}).trimEnd();
14
+ }
@@ -0,0 +1,13 @@
1
+ import {stripVTControlCharacters} from 'node:util';
2
+
3
+ /** Status payload strings are data, never terminal control sequences. */
4
+ export const footerText = value => typeof value==='string'
5
+ ? stripVTControlCharacters(value).replace(/[\p{Cc}\p{Cf}]/gu,' ').slice(0,160).trim() : '';
6
+ export function footerCwd(value) {
7
+ const text=footerText(value), parts=text.split(/[\\/]/).filter(Boolean);
8
+ return parts.length>3?`…/${parts.slice(-3).join('/')}`:text;
9
+ }
10
+ export function indicatorGlyph(report) {
11
+ const rgb=report.health==='ready'?'44;122;57':report.health==='problem'?'150;108;30':'102;102;102';
12
+ return report.label.replace(/^ACC ([●!…])/u,(_,glyph)=>`ACC \x1b[22;1;38;2;${rgb}m${glyph}\x1b[22;39m`);
13
+ }
@@ -29,6 +29,8 @@ export const NATIVE_REASON_CODES = Object.freeze([
29
29
  "prerelease_not_captured", "below_minimum_version", "known_bad_version",
30
30
  "feature_probe_failed", "probe_timeout", "probe_version_mismatch", "protocol_mismatch",
31
31
  "native_endpoint_unavailable", "native_session_unavailable",
32
+ // A caller can be denied access to a healthy receiver's local transport.
33
+ "transport_permission_denied",
32
34
  "handshake_failed", "handshake_timeout", "handshake_version_mismatch",
33
35
  "session_generation_stale", "client_process_unknown", "unsupported_shell",
34
36
  // The current session's canonical workspace identity could not be verified.
@@ -0,0 +1,12 @@
1
+ let last = 0n;
2
+
3
+ // A tie-break for equal wall-clock milliseconds, captured in the caller before
4
+ // worker scheduling. Node's hrtime uses the OS monotonic clock; increment locally
5
+ // if its resolution produces a repeated tick. Wall time still orders other dates.
6
+ export function captureOfferOrder() {
7
+ const tick = process.hrtime.bigint();
8
+ last = tick > last ? tick : last + 1n;
9
+ return last.toString();
10
+ }
11
+
12
+ export const validOfferOrder = value => typeof value === "string" && /^(0|[1-9]\d{0,19})$/.test(value);
@@ -0,0 +1,37 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, rename, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { parentPort, workerData as input } from "node:worker_threads";
5
+ import { readSessionRecord, withWriterMutex } from "@agents-can-communicate/storage-filesystem";
6
+ import { loadOfferObservation, offerObservationPath, OFFER_ERRORS } from "./offer-observation.mjs";
7
+ import { validOfferOrder } from "./offer-observation-order.mjs";
8
+
9
+ async function write() {
10
+ const { runtimeDir: root, workspaceId, sessionId, generation, at, order, state, reasonCode, deadlineAt } = input;
11
+ const file = offerObservationPath(root, sessionId);
12
+ if (!validOfferOrder(order) || !Number.isFinite(Date.parse(at)) || !["active", "degraded"].includes(state)
13
+ || !(reasonCode === null || OFFER_ERRORS.includes(reasonCode))) return false;
14
+ const check = () => { if (Date.now() >= deadlineAt) throw new Error("observation deadline"); };
15
+ check();
16
+ return withWriterMutex({ locks: path.join(root, "offer-observation-locks", sessionId) },
17
+ { root, deadlineAt, clock: { now: () => new Date().toISOString() } }, async () => {
18
+ check();
19
+ const session = await readSessionRecord({ root, workspaceId, sessionId });
20
+ if (session?.state !== "open" || session.generation !== generation) return false;
21
+ const previous = await loadOfferObservation(input);
22
+ if (previous && (Date.parse(previous.at) > Date.parse(at)
23
+ || (Date.parse(previous.at) === Date.parse(at) && previous.order !== undefined
24
+ && BigInt(previous.order) >= BigInt(order)))) return false;
25
+ await mkdir(path.dirname(file), { recursive: true });
26
+ const temporary = `${file}.${randomUUID()}.tmp`;
27
+ try {
28
+ check();
29
+ await writeFile(temporary, JSON.stringify({ schemaVersion: 1, sessionId, generation,
30
+ at, order, state, reasonCode }) + "\n", { mode: 0o600 });
31
+ check();
32
+ await rename(temporary, file);
33
+ return true;
34
+ } finally { await rm(temporary, { force: true }).catch(() => {}); }
35
+ });
36
+ }
37
+ parentPort.postMessage(await write().catch(() => false));
@@ -0,0 +1,47 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { Worker } from "node:worker_threads";
4
+ import { assertPortableId } from "@agents-can-communicate/protocol";
5
+ import { captureOfferOrder, validOfferOrder } from "./offer-observation-order.mjs";
6
+
7
+ export const OFFER_ERRORS = Object.freeze(["ambiguous_recipient_sessions", "delivery_disabled",
8
+ "recipient_busy", "recipient_unavailable", "transport_error", "transport_rejected",
9
+ "transport_permission_denied", "unsupported_client_version", "inbound_approval_required"]);
10
+
11
+ export const offerObservationPath = (runtimeDir, sessionId) => {
12
+ assertPortableId(sessionId, "offer observation session id");
13
+ return path.join(runtimeDir, "offer-observations", `${sessionId}.json`);
14
+ };
15
+
16
+ export async function loadOfferObservation({ runtimeDir, sessionId, generation }) {
17
+ try {
18
+ const record = JSON.parse(await readFile(offerObservationPath(runtimeDir, sessionId), "utf8"));
19
+ if (record.schemaVersion !== 1 || record.sessionId !== sessionId || record.generation !== generation
20
+ || !Number.isFinite(Date.parse(record.at)) || !["active", "degraded"].includes(record.state)
21
+ || !(record.reasonCode === null || OFFER_ERRORS.includes(record.reasonCode))
22
+ || (record.order !== undefined && !validOfferOrder(record.order))) return null;
23
+ return { at: record.at, state: record.state, reasonCode: record.reasonCode,
24
+ ...(record.order === undefined ? {} : { order: record.order }) };
25
+ } catch { return null; }
26
+ }
27
+
28
+ /** Optional UI diagnostic, separate from delivery receipts and history. Failure
29
+ * to publish it cannot turn a recorded message into a failed send. */
30
+ export async function storeOfferObservation(input) {
31
+ if (typeof input.runtimeDir !== "string" || typeof input.workspaceId !== "string") return false;
32
+ let worker, timer;
33
+ try {
34
+ worker = new Worker(new URL("./offer-observation-writer.mjs", import.meta.url), {
35
+ workerData: { ...input, order: captureOfferOrder(), deadlineAt: Date.now() + 1500 },
36
+ });
37
+ const done = new Promise(resolve => {
38
+ worker.once("message", value => resolve(value === true));
39
+ worker.once("error", () => resolve(false));
40
+ worker.once("exit", () => resolve(false));
41
+ timer = setTimeout(resolve, 1500, false);
42
+ });
43
+ worker.unref();
44
+ return await done;
45
+ } catch { return false; }
46
+ finally { clearTimeout(timer); }
47
+ }
@@ -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
+ }