agents-can-communicate 0.10.4 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -21
- package/bin/acc-indicator.mjs +5 -0
- package/docs/CAPABILITIES.md +3 -0
- package/docs/CLI.md +34 -7
- package/docs/MCP.md +42 -2
- package/docs/PROTOCOL.md +19 -0
- package/docs/STATUS_INDICATOR.md +128 -0
- package/docs/WHY_ACC.md +14 -8
- package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +3 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-install.mjs +80 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/indicator-render.mjs +8 -0
- package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +7 -4
- package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/hooks/indicator.mjs +67 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/plugin/skills/acc/SKILL.md +29 -5
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +1 -0
- package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +16 -5
- package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
- package/node_modules/@agents-can-communicate/adapter-codex/plugin/skills/acc/SKILL.md +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/offer-observation-order.mjs +12 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation-writer.mjs +37 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/offer-observation.mjs +47 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-indicator.mjs +141 -0
- package/node_modules/@agents-can-communicate/adapter-sdk/src/toml-scan.mjs +99 -0
- package/node_modules/@agents-can-communicate/cli/package.json +2 -1
- package/node_modules/@agents-can-communicate/cli/src/args.mjs +5 -3
- package/node_modules/@agents-can-communicate/cli/src/claim-command.mjs +64 -0
- package/node_modules/@agents-can-communicate/cli/src/help.mjs +15 -8
- package/node_modules/@agents-can-communicate/cli/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-command.mjs +65 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-previous-command.mjs +84 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator-previous-windows.mjs +8 -0
- package/node_modules/@agents-can-communicate/cli/src/indicator.mjs +112 -0
- package/node_modules/@agents-can-communicate/cli/src/install-command.mjs +4 -0
- package/node_modules/@agents-can-communicate/cli/src/main.mjs +3 -53
- 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/core/package.json +1 -1
- package/node_modules/@agents-can-communicate/core/src/claim-selectors.mjs +62 -0
- package/node_modules/@agents-can-communicate/core/src/claims.mjs +88 -81
- package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
- package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +14 -5
- package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/package.json +1 -1
- package/node_modules/@agents-can-communicate/installer/src/apply.mjs +3 -0
- package/node_modules/@agents-can-communicate/installer/src/ownership.mjs +2 -1
- package/node_modules/@agents-can-communicate/installer/src/plan.mjs +11 -2
- package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
- package/node_modules/@agents-can-communicate/mcp-server/src/claim-tools.mjs +56 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/input-validator.mjs +9 -0
- package/node_modules/@agents-can-communicate/mcp-server/src/server.mjs +7 -8
- package/node_modules/@agents-can-communicate/mcp-server/src/tools.mjs +27 -9
- package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
- package/node_modules/@agents-can-communicate/protocol/src/index.mjs +1 -0
- package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -1
- package/node_modules/@agents-can-communicate/storage-filesystem/src/session-read.mjs +13 -0
- package/package.json +3 -1
|
@@ -0,0 +1,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(
|
|
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(
|
|
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(
|
|
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" },
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
2
|
+
|
|
3
|
+
export const unsafe = (reason = "unrecognized TOML declaration") => {
|
|
4
|
+
throw new AccError(EXIT.CONFLICT, `cannot safely edit status-line TOML: ${reason}; `
|
|
5
|
+
+ "repair ambiguous TOML before retrying", { reason });
|
|
6
|
+
};
|
|
7
|
+
// A bounded lexical scan, not a TOML value parser. Newlines inside strings,
|
|
8
|
+
// arrays and inline tables cannot introduce a header or a managed marker.
|
|
9
|
+
export function* statements(source) {
|
|
10
|
+
let start = 0, code = "", quote = null;
|
|
11
|
+
const stack = [];
|
|
12
|
+
for (let i = 0; i < source.length;) {
|
|
13
|
+
const char = source[i];
|
|
14
|
+
if (quote) {
|
|
15
|
+
if (quote[0] === '"' && char === "\\") {
|
|
16
|
+
code += source.slice(i, i + 2); i += 2; continue;
|
|
17
|
+
}
|
|
18
|
+
if (source.startsWith(quote, i)) {
|
|
19
|
+
let count = quote.length;
|
|
20
|
+
if (quote.length === 3) {
|
|
21
|
+
while (source[i + count] === quote[0]) count++;
|
|
22
|
+
if (count > 5) unsafe("unsupported string quote delimiter");
|
|
23
|
+
}
|
|
24
|
+
code += source.slice(i, i + count); i += count; quote = null; continue;
|
|
25
|
+
}
|
|
26
|
+
if (quote.length === 1 && /[\r\n]/.test(char)) unsafe("newline in single-line string");
|
|
27
|
+
code += char; i++; continue;
|
|
28
|
+
}
|
|
29
|
+
if (char === "#") {
|
|
30
|
+
while (i < source.length && source[i] !== "\n") i++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (char === '"' || char === "'") {
|
|
34
|
+
quote = source.startsWith(char.repeat(3), i) ? char.repeat(3) : char;
|
|
35
|
+
code += quote; i += quote.length; continue;
|
|
36
|
+
}
|
|
37
|
+
if (char === "[" || char === "{") stack.push(char);
|
|
38
|
+
if (char === "]" || char === "}") {
|
|
39
|
+
if (stack.pop() !== (char === "]" ? "[" : "{")) unsafe("mismatched collection delimiter");
|
|
40
|
+
}
|
|
41
|
+
code += char; i++;
|
|
42
|
+
if (char === "\n" && stack.length === 0) {
|
|
43
|
+
yield { raw: source.slice(start, i), code: code.trim(), start, end: i };
|
|
44
|
+
start = i; code = "";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (quote || stack.length) unsafe(quote ? "unclosed string" : "unclosed collection");
|
|
48
|
+
if (start < source.length) yield { raw: source.slice(start), code: code.trim(), start, end: source.length };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function decodeKey(key) {
|
|
52
|
+
if (key[0] === "'") return key.slice(1, -1);
|
|
53
|
+
if (key[0] !== '"') return key;
|
|
54
|
+
return key.slice(1, -1).replace(/\\(U[\da-fA-F]{8}|u[\da-fA-F]{4}|[btnfr"\\])|\\./g,
|
|
55
|
+
(match, escape) => {
|
|
56
|
+
if (!escape) unsafe("unsupported quoted-key escape");
|
|
57
|
+
if (/^[uU]/.test(escape)) {
|
|
58
|
+
const point = Number.parseInt(escape.slice(1), 16);
|
|
59
|
+
if (point > 0x10ffff || (point >= 0xd800 && point <= 0xdfff)) {
|
|
60
|
+
unsafe("invalid quoted-key Unicode escape");
|
|
61
|
+
}
|
|
62
|
+
return String.fromCodePoint(point);
|
|
63
|
+
}
|
|
64
|
+
return ({ b: "\b", t: "\t", n: "\n", f: "\f", r: "\r", '"': '"', "\\": "\\" })[escape];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function keyPath(code) {
|
|
69
|
+
const keys = [];
|
|
70
|
+
let rest = code.trimStart();
|
|
71
|
+
for (;;) {
|
|
72
|
+
const match = /^(?:[A-Za-z0-9_-]+|'[^'\r\n]*'|"(?:[^"\\\r\n]|\\[^\r\n])*")/.exec(rest);
|
|
73
|
+
if (!match) unsafe("unrecognized TOML key");
|
|
74
|
+
keys.push(decodeKey(match[0]));
|
|
75
|
+
rest = rest.slice(match[0].length).trimStart();
|
|
76
|
+
if (!rest.startsWith(".")) return { keys, rest };
|
|
77
|
+
rest = rest.slice(1).trimStart();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function declaration(code) {
|
|
82
|
+
const header = code.startsWith("[");
|
|
83
|
+
const array = header && code.startsWith("[[");
|
|
84
|
+
const { keys, rest } = keyPath(header ? code.slice(array ? 2 : 1) : code);
|
|
85
|
+
if (header ? rest !== (array ? "]]" : "]") : !rest.startsWith("=")) unsafe();
|
|
86
|
+
return { keys, header, array, value: header ? null : rest.slice(1).trim() };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Resolved declaration paths are only structural facts, never parsed user values.
|
|
90
|
+
export function scanConfig(source) {
|
|
91
|
+
let table = [];
|
|
92
|
+
return [...statements(source)].map(item => {
|
|
93
|
+
if (!item.code) return { ...item, table: [...table], keys: [] };
|
|
94
|
+
const entry = declaration(item.code);
|
|
95
|
+
if (entry.header) table = entry.keys;
|
|
96
|
+
return { ...item, ...entry, table: [...table],
|
|
97
|
+
keys: entry.header ? entry.keys : [...table, ...entry.keys] };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agents-can-communicate/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.mjs",
|
|
8
8
|
"./hook-support": "./src/hook-support.mjs",
|
|
9
|
+
"./indicator": "./src/indicator-command.mjs",
|
|
9
10
|
"./managed-entry": "./src/managed-runtime/entry.mjs",
|
|
10
11
|
"./managed-worker": "./src/managed-runtime/worker.mjs"
|
|
11
12
|
},
|
|
@@ -18,13 +18,15 @@ export const COMMANDS = Object.freeze({
|
|
|
18
18
|
work: { required: [], optional: ["session", "generation", "summary", "mode",
|
|
19
19
|
"state"], repeated: ["hint"], flags: ["clear"] },
|
|
20
20
|
claim: { required: ["resource"],
|
|
21
|
-
optional: ["session", "generation", "mode", "enforcement", "reason", "lease"]
|
|
21
|
+
optional: ["session", "generation", "mode", "enforcement", "reason", "lease"],
|
|
22
|
+
repeated: ["resource"] },
|
|
22
23
|
// Neither is required on its own, because either one names the claim: an id
|
|
23
24
|
// is precise, and a resource is what the caller typed to take it in the first
|
|
24
25
|
// place. Requiring the id meant a round trip through `acc status --json` to
|
|
25
26
|
// look up something the caller never chose.
|
|
26
27
|
release: { required: [],
|
|
27
|
-
optional: ["
|
|
28
|
+
optional: ["session", "generation", "authority", "reason"],
|
|
29
|
+
repeated: ["claim", "resource"], flags: ["all"] },
|
|
28
30
|
message: { required: ["subject", "body"],
|
|
29
31
|
optional: ["session", "generation", "type", "obligation", "client-message-id"],
|
|
30
32
|
repeated: ["to", "supersedes", "withdraws"] },
|
|
@@ -57,7 +59,7 @@ export const COMMANDS = Object.freeze({
|
|
|
57
59
|
// `--downgrade` because an older acc first on PATH will otherwise rewire every
|
|
58
60
|
// client to itself, and the only symptom is a guard behaving like the version
|
|
59
61
|
// it came from.
|
|
60
|
-
install: { required: [], optional: ["home", "delivery"], repeated: ["adapter"],
|
|
62
|
+
install: { required: [], optional: ["home", "delivery", "indicator"], repeated: ["adapter"],
|
|
61
63
|
flags: ["dry-run", "downgrade"] },
|
|
62
64
|
// `--dry-run` on both, because the preview was computed for either action and
|
|
63
65
|
// only `install` could ask for it. Removal is the side that reaches into a
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { AccError, EXIT } from "@agents-can-communicate/protocol";
|
|
2
|
+
|
|
3
|
+
import { positiveNumber } from "./args.mjs";
|
|
4
|
+
import { canonicalClaim } from "./claim-spelling.mjs";
|
|
5
|
+
|
|
6
|
+
const list = value => value === undefined ? undefined : Array.isArray(value) ? value : [value];
|
|
7
|
+
const owner = options => ({ sessionId: options.session, generation: options.generation });
|
|
8
|
+
|
|
9
|
+
export async function runClaimCommand({ options, context }) {
|
|
10
|
+
const resources = await Promise.all(list(options.resource)
|
|
11
|
+
.map(resource => canonicalClaim(resource, context.descriptor)));
|
|
12
|
+
const input = { ...owner(options), mode: options.mode ?? "exclusive",
|
|
13
|
+
enforcement: options.enforcement ?? "advisory", reason: options.reason ?? "unspecified",
|
|
14
|
+
leaseSeconds: options.lease ? positiveNumber(options.lease, "lease") : undefined,
|
|
15
|
+
descriptor: context.descriptor };
|
|
16
|
+
if (resources.length === 1) {
|
|
17
|
+
const claim = await context.service.acquireClaim({ ...input, resource: resources[0] });
|
|
18
|
+
return { data: claim, text: `claimed ${claim.resource}` };
|
|
19
|
+
}
|
|
20
|
+
const claims = await context.service.acquireClaims({ ...input, resources });
|
|
21
|
+
return { data: { claims }, text: `claimed ${claims.length} resources` };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Single-item authority keeps its existing lookup and ambiguity rules.
|
|
25
|
+
async function authorityClaim(resource, context) {
|
|
26
|
+
const { claims } = await context.service.collectStatus({});
|
|
27
|
+
const matching = claims.filter(claim => claim.resource === resource);
|
|
28
|
+
if (matching.length === 0) throw new AccError(EXIT.DATA, `no claim on ${resource} to release`,
|
|
29
|
+
{ resource, held: claims.map(claim => claim.resource) });
|
|
30
|
+
if (matching.length > 1) throw new AccError(EXIT.DATA,
|
|
31
|
+
`${matching.length} claims on ${resource}; name the one you mean with --claim`,
|
|
32
|
+
{ resource, claims: matching.map(claim => claim.claimId) });
|
|
33
|
+
return matching[0].claimId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function runReleaseCommand({ options, context }) {
|
|
37
|
+
const selectors = [options.claim, options.resource, options.all].filter(value => value !== undefined);
|
|
38
|
+
if (selectors.length !== 1) throw new AccError(EXIT.USAGE,
|
|
39
|
+
"release needs exactly one of --claim, --resource, or --all");
|
|
40
|
+
const claimIds = list(options.claim);
|
|
41
|
+
const resources = options.resource === undefined ? undefined
|
|
42
|
+
: await Promise.all(list(options.resource).map(resource => canonicalClaim(resource, context.descriptor)));
|
|
43
|
+
const scalar = options.all !== true && (claimIds ?? resources).length === 1;
|
|
44
|
+
if (options.authority !== undefined) {
|
|
45
|
+
if (!scalar) throw new AccError(EXIT.USAGE, "force-release authority requires a single claim");
|
|
46
|
+
const claimId = claimIds?.[0] ?? await authorityClaim(resources[0], context);
|
|
47
|
+
await context.service.forceReleaseClaim({ ...owner(options), claimId,
|
|
48
|
+
authority: options.authority, reason: options.reason });
|
|
49
|
+
return { data: { claimId }, text: `released ${claimId}` };
|
|
50
|
+
}
|
|
51
|
+
if (scalar && claimIds !== undefined) {
|
|
52
|
+
const claimId = claimIds[0];
|
|
53
|
+
await context.service.releaseClaim({ ...owner(options), claimId });
|
|
54
|
+
return { data: { claimId }, text: `released ${claimId}` };
|
|
55
|
+
}
|
|
56
|
+
const released = await context.service.releaseClaims({ ...owner(options),
|
|
57
|
+
...(options.all === true ? { all: true } : resources !== undefined ? { resources } : { claimIds }) });
|
|
58
|
+
if (scalar) {
|
|
59
|
+
const claimId = released[0].claimId;
|
|
60
|
+
return { data: { claimId }, text: `released ${claimId}` };
|
|
61
|
+
}
|
|
62
|
+
return { data: { claimIds: released.map(claim => claim.claimId) },
|
|
63
|
+
text: `released ${released.length} claims` };
|
|
64
|
+
}
|