@astrosheep/square 0.3.63 → 0.3.64
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/claude-plugin/.claude-plugin/plugin.json +3 -2
- package/claude-plugin/hooks/hooks.json +1 -22
- package/claude-plugin/hooks/register.js +119 -0
- package/claude-plugin/types.d.ts +7 -0
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity-feed.d.ts +1 -10
- package/dist/activity-feed.js +1 -27
- package/dist/activity.js +57 -25
- package/dist/artifact.js +12 -24
- package/dist/attention-presentation.js +1 -0
- package/dist/automatic-session.d.ts +3 -1
- package/dist/automatic-session.js +21 -34
- package/dist/boundary-presentation.js +4 -13
- package/dist/canonical-path.d.ts +11 -0
- package/dist/canonical-path.js +47 -0
- package/dist/catch-decisions.js +14 -10
- package/dist/catch-selection.d.ts +6 -0
- package/dist/catch-selection.js +14 -0
- package/dist/claude-delivery.d.ts +11 -0
- package/dist/claude-delivery.js +145 -0
- package/dist/claude-mod.d.ts +8 -0
- package/dist/claude-mod.js +164 -0
- package/dist/cli/context.d.ts +0 -1
- package/dist/cli/context.js +9 -21
- package/dist/cli/harness-command.d.ts +0 -1
- package/dist/cli/harness-command.js +0 -6
- package/dist/cli/observation-commands.d.ts +1 -1
- package/dist/cli/observation-commands.js +31 -58
- package/dist/cli/program.js +20 -18
- package/dist/cli/registry.js +2 -2
- package/dist/cli/square-commands.js +10 -6
- package/dist/codex-hook.js +9 -12
- package/dist/codex-queue.d.ts +2 -2
- package/dist/codex-queue.js +7 -5
- package/dist/decisions.d.ts +2 -6
- package/dist/decisions.js +16 -17
- package/dist/delivery-health.d.ts +1 -2
- package/dist/delivery-health.js +4 -5
- package/dist/delivery-operations.d.ts +2 -5
- package/dist/delivery-operations.js +77 -214
- package/dist/delivery.d.ts +25 -47
- package/dist/delivery.js +14 -46
- package/dist/diagnostic-redaction.js +7 -9
- package/dist/file-changes.d.ts +0 -1
- package/dist/file-changes.js +2 -19
- package/dist/harness-claude.js +8 -6
- package/dist/harness-codex.d.ts +0 -2
- package/dist/harness-codex.js +0 -2
- package/dist/harness-links.js +13 -17
- package/dist/harness.js +3 -7
- package/dist/help.js +4 -3
- package/dist/host-ledger-file-adapter.d.ts +17 -8
- package/dist/host-ledger-file-adapter.js +164 -80
- package/dist/host-ledger.d.ts +55 -50
- package/dist/inbox.d.ts +0 -3
- package/dist/inbox.js +13 -30
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/list.js +1 -4
- package/dist/mcp.js +18 -40
- package/dist/mention-parse.js +3 -9
- package/dist/model.d.ts +11 -2
- package/dist/model.js +1 -1
- package/dist/notifications.d.ts +3 -16
- package/dist/notifications.js +41 -122
- package/dist/opencode-delivery.d.ts +14 -0
- package/dist/opencode-delivery.js +171 -0
- package/dist/opencode.d.ts +39 -18
- package/dist/opencode.js +145 -40
- package/dist/packages/agent-delivery/src/claude-auth.d.ts +13 -0
- package/dist/packages/agent-delivery/src/claude-auth.js +70 -0
- package/dist/packages/agent-delivery/src/claude-native.d.ts +28 -0
- package/dist/packages/agent-delivery/src/claude-native.js +65 -0
- package/dist/packages/agent-delivery/src/claude.d.ts +14 -0
- package/dist/packages/agent-delivery/src/claude.js +66 -0
- package/dist/packages/agent-delivery/src/deadline.d.ts +11 -0
- package/dist/packages/agent-delivery/src/deadline.js +41 -0
- package/dist/packages/agent-delivery/src/errors.d.ts +16 -0
- package/dist/packages/agent-delivery/src/errors.js +41 -0
- package/dist/packages/agent-delivery/src/guards.d.ts +6 -0
- package/dist/packages/agent-delivery/src/guards.js +6 -0
- package/dist/packages/agent-delivery/src/index.d.ts +83 -0
- package/dist/packages/agent-delivery/src/index.js +110 -0
- package/dist/packages/agent-delivery/src/local-endpoint.d.ts +26 -0
- package/dist/packages/agent-delivery/src/local-endpoint.js +114 -0
- package/dist/packages/agent-delivery/src/opencode-native.d.ts +33 -0
- package/dist/packages/agent-delivery/src/opencode-native.js +75 -0
- package/dist/packages/agent-delivery/src/opencode.d.ts +14 -0
- package/dist/packages/agent-delivery/src/opencode.js +127 -0
- package/dist/packages/agent-delivery/src/paseo-native.d.ts +58 -0
- package/dist/packages/agent-delivery/src/paseo-native.js +122 -0
- package/dist/packages/agent-delivery/src/paseo.d.ts +19 -0
- package/dist/packages/agent-delivery/src/paseo.js +106 -0
- package/dist/packages/agent-delivery/src/pi-client.d.ts +10 -0
- package/dist/packages/agent-delivery/src/pi-client.js +106 -0
- package/dist/packages/agent-delivery/src/pi-protocol.d.ts +15 -0
- package/dist/packages/agent-delivery/src/pi-protocol.js +50 -0
- package/dist/packages/agent-delivery/src/pi-send.d.ts +23 -0
- package/dist/packages/agent-delivery/src/pi-send.js +4 -0
- package/dist/packages/agent-delivery/src/pi.d.ts +25 -0
- package/dist/packages/agent-delivery/src/pi.js +209 -0
- package/dist/packages/agent-delivery/src/types.d.ts +120 -0
- package/dist/participant-host.js +12 -13
- package/dist/participant-identity.d.ts +21 -0
- package/dist/participant-identity.js +32 -11
- package/dist/paseo-delivery.d.ts +2 -2
- package/dist/paseo-delivery.js +35 -27
- package/dist/paseo-timeline.js +25 -21
- package/dist/paseo.d.ts +2 -2
- package/dist/paseo.js +1 -2
- package/dist/ports.d.ts +7 -20
- package/dist/presentation-operations.js +16 -4
- package/dist/presentation.d.ts +19 -10
- package/dist/presentation.js +99 -177
- package/dist/registry.d.ts +15 -66
- package/dist/registry.js +45 -123
- package/dist/routes.d.ts +1 -28
- package/dist/routes.js +22 -104
- package/dist/runtime.d.ts +3 -9
- package/dist/runtime.js +14 -67
- package/dist/search.d.ts +0 -2
- package/dist/search.js +3 -10
- package/dist/square-actions.js +48 -72
- package/dist/square-application.js +25 -23
- package/dist/square-core.d.ts +3 -0
- package/dist/square-core.js +22 -5
- package/dist/square-facade.d.ts +2 -0
- package/dist/square-file-adapter.js +4 -7
- package/dist/square-projections.d.ts +40 -45
- package/dist/square-projections.js +43 -73
- package/dist/square-state.js +1 -4
- package/dist/square-storage.d.ts +5 -8
- package/dist/square-storage.js +13 -36
- package/dist/square-wiring.d.ts +0 -5
- package/dist/square-wiring.js +3 -21
- package/dist/stream.js +4 -6
- package/dist/time.js +2 -7
- package/dist/views.d.ts +4 -16
- package/dist/views.js +12 -19
- package/dist/wake-attempts.d.ts +2 -56
- package/dist/wake-attempts.js +15 -110
- package/dist/wake-eligibility.d.ts +43 -0
- package/dist/wake-eligibility.js +106 -0
- package/dist/wake-evidence.d.ts +32 -9
- package/dist/wake-evidence.js +29 -24
- package/dist/wake-sink.d.ts +13 -9
- package/dist/wake-sink.js +35 -54
- package/dist/watch.js +13 -21
- package/extensions/square-pi.js +10 -5
- package/guides/claude-native.md +101 -0
- package/guides/host-ledger.md +3 -1
- package/guides/participant.md +14 -7
- package/guides/pi-native-delivery.md +43 -0
- package/package.json +4 -3
- package/dist/claude-hook.d.ts +0 -10
- package/dist/claude-hook.js +0 -60
- package/dist/paseo-connection.d.ts +0 -15
- package/dist/paseo-connection.js +0 -141
- package/dist/presence.d.ts +0 -2
- package/dist/presence.js +0 -24
- package/dist/presented.d.ts +0 -2
- package/dist/presented.js +0 -10
- package/dist/state-cell.d.ts +0 -14
- package/dist/wake-port.d.ts +0 -23
- package/dist/wake-port.js +0 -27
- /package/dist/{state-cell.js → packages/agent-delivery/src/types.js} +0 -0
package/dist/opencode.js
CHANGED
|
@@ -1,49 +1,154 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { automaticSessionStart, operationEnv } from './automatic-session.js';
|
|
2
|
+
import { hostLedgerForEnv } from './registry.js';
|
|
3
|
+
import { canonicalPath } from './canonical-path.js';
|
|
4
|
+
import { openSquare } from './square-file-adapter.js';
|
|
5
|
+
import { closeOpenSquare } from './open-square.js';
|
|
6
|
+
import { Square } from './square-wiring.js';
|
|
7
|
+
import { connectNative } from './packages/agent-delivery/src/opencode-native.js';
|
|
8
|
+
import { observeOpenCodeContext, publishOpenCodeRoutes, receiveOpenCodePending } from './opencode-delivery.js';
|
|
9
|
+
/** Actual OpenCode 2.0.20 server plugin. Harness identity belongs to ctx, never process.env. */
|
|
10
|
+
export default {
|
|
11
|
+
id: 'square',
|
|
12
|
+
async setup(ctx) {
|
|
13
|
+
if (ctx.app.version !== '2.0.20')
|
|
14
|
+
return async () => { };
|
|
15
|
+
const env = { ...process.env };
|
|
16
|
+
const ledger = hostLedgerForEnv(env);
|
|
17
|
+
const lifetime = new AbortController();
|
|
18
|
+
const receivers = new Map(); // Operational handles only; presence owns membership.
|
|
19
|
+
const directory = await canonicalPath(ctx.location.directory);
|
|
20
|
+
let coordinating = Promise.resolve();
|
|
21
|
+
const coordinate = (operation) => {
|
|
22
|
+
const task = coordinating.then(() => lifetime.signal.aborted ? undefined : operation());
|
|
23
|
+
coordinating = task.catch(() => { }); // Optional Square hooks must never fail primary request preparation.
|
|
24
|
+
return coordinating;
|
|
25
|
+
};
|
|
26
|
+
const stop = (sessionId) => {
|
|
27
|
+
const receiver = receivers.get(sessionId);
|
|
28
|
+
receiver?.abort.abort();
|
|
29
|
+
receivers.delete(sessionId);
|
|
30
|
+
return receiver?.running;
|
|
31
|
+
};
|
|
32
|
+
const validate = async (sessionId) => {
|
|
33
|
+
let info;
|
|
34
|
+
const target = await connectNative({ sessionId, timeoutMs: 3000, signal: lifetime.signal, session: {
|
|
35
|
+
async get(input) { info = await ctx.session.get(input); return info; },
|
|
36
|
+
prompt: (input) => ctx.session.prompt(input),
|
|
37
|
+
} }).catch(() => undefined);
|
|
38
|
+
const record = info;
|
|
39
|
+
if (!target || !record?.location?.directory || record.projectID !== ctx.location.project.id
|
|
40
|
+
|| record.location.workspaceID !== ctx.location.workspaceID
|
|
41
|
+
|| await canonicalPath(record.location.directory) !== directory)
|
|
42
|
+
return undefined;
|
|
43
|
+
return target;
|
|
44
|
+
};
|
|
45
|
+
const reconcile = async (activeSession) => {
|
|
46
|
+
const candidates = new Set((await ledger.listPresence({})).filter((binding) => binding.channel === 'opencode').map((binding) => binding.session));
|
|
47
|
+
if (activeSession)
|
|
48
|
+
candidates.add(activeSession);
|
|
49
|
+
for (const sessionId of candidates) {
|
|
50
|
+
if (lifetime.signal.aborted)
|
|
51
|
+
return;
|
|
52
|
+
const target = await validate(sessionId);
|
|
53
|
+
if (!target) {
|
|
54
|
+
stop(sessionId);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const scopedEnv = operationEnv('opencode', sessionId, env);
|
|
58
|
+
if (sessionId === activeSession)
|
|
59
|
+
await automaticSessionStart('opencode', sessionId, directory, scopedEnv);
|
|
60
|
+
if (!await publishOpenCodeRoutes(sessionId, scopedEnv, lifetime.signal)) {
|
|
61
|
+
stop(sessionId);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (receivers.has(sessionId))
|
|
65
|
+
continue;
|
|
66
|
+
const abort = new AbortController();
|
|
67
|
+
const signal = AbortSignal.any([lifetime.signal, abort.signal]);
|
|
68
|
+
const running = receiveOpenCodePending(target, scopedEnv, signal).catch(() => { });
|
|
69
|
+
receivers.set(sessionId, { abort, running });
|
|
70
|
+
}
|
|
71
|
+
for (const sessionId of receivers.keys())
|
|
72
|
+
if (!candidates.has(sessionId))
|
|
73
|
+
stop(sessionId);
|
|
74
|
+
};
|
|
75
|
+
const cancel = async (sessionId) => {
|
|
76
|
+
if (!receivers.has(sessionId))
|
|
77
|
+
return;
|
|
78
|
+
// Stop synchronously; persist the cutoff before allowing reconciliation to start fresh work.
|
|
79
|
+
stop(sessionId);
|
|
80
|
+
await ledger.withClaimLock(async () => {
|
|
81
|
+
for (const owner of (await ledger.listPresence({ session: sessionId })).filter((binding) => binding.channel === 'opencode')) {
|
|
82
|
+
const square = await openSquare(owner.location, { hostLedger: ledger, env, signal: lifetime.signal });
|
|
12
83
|
try {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
84
|
+
await ledger.suppressPresence(owner, (await square.artifact.read(lifetime.signal)).state.runtime.nextActIndex - 1);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
await closeOpenSquare(square);
|
|
16
88
|
}
|
|
17
|
-
|
|
89
|
+
}
|
|
90
|
+
}, lifetime.signal);
|
|
91
|
+
await reconcile();
|
|
92
|
+
};
|
|
93
|
+
const end = async (sessionId) => {
|
|
94
|
+
stop(sessionId);
|
|
95
|
+
const memberships = (await ledger.listPresence({ session: sessionId })).filter((binding) => binding.channel === 'opencode');
|
|
96
|
+
for (const owner of memberships) {
|
|
97
|
+
const square = await Square.at({ path: owner.location, hostLedger: ledger, env: operationEnv('opencode', sessionId, env) });
|
|
98
|
+
try {
|
|
99
|
+
await square.endOwnedSession(owner.participant, sessionId, owner.epoch);
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
await square.close();
|
|
18
103
|
}
|
|
19
104
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
105
|
+
};
|
|
106
|
+
await ctx.session.hook('prompt', (event) => coordinate(() => reconcile(event.sessionID)));
|
|
107
|
+
await ctx.session.hook('context', async (event) => {
|
|
108
|
+
await coordinate(() => reconcile(event.sessionID));
|
|
109
|
+
const receiver = receivers.get(event.sessionID);
|
|
110
|
+
if (!receiver || lifetime.signal.aborted)
|
|
111
|
+
return;
|
|
112
|
+
await observeOpenCodeContext(event.sessionID, event.messages, operationEnv('opencode', event.sessionID, env), AbortSignal.any([lifetime.signal, receiver.abort.signal])).catch(() => { });
|
|
113
|
+
});
|
|
114
|
+
const changes = await ledger.observeChanges();
|
|
115
|
+
changes.evidence.close(); // Manual joins activate an idle receiver; evidence already has a session observer.
|
|
116
|
+
const presenceLoop = (async () => {
|
|
117
|
+
while (!lifetime.signal.aborted) {
|
|
118
|
+
const baseline = await changes.presence.read(lifetime.signal);
|
|
119
|
+
await coordinate(() => reconcile());
|
|
120
|
+
await changes.presence.changed(baseline, Infinity, lifetime.signal);
|
|
121
|
+
}
|
|
122
|
+
})().catch(() => { });
|
|
123
|
+
const eventLoop = (async () => {
|
|
124
|
+
for await (const event of ctx.event.subscribe({ signal: lifetime.signal })) {
|
|
125
|
+
const sessionId = event.data.sessionID;
|
|
126
|
+
if (!sessionId)
|
|
127
|
+
continue;
|
|
128
|
+
if (event.type === 'session.deleted')
|
|
129
|
+
await coordinate(() => end(sessionId));
|
|
130
|
+
else if (event.type === 'session.moved')
|
|
131
|
+
await coordinate(async () => { stop(sessionId); await reconcile(); });
|
|
132
|
+
else if (event.type === 'session.execution.interrupted' && event.data.reason === 'user') {
|
|
133
|
+
receivers.get(sessionId)?.abort.abort();
|
|
134
|
+
await coordinate(() => cancel(sessionId));
|
|
26
135
|
}
|
|
27
136
|
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
137
|
+
})().catch(() => { });
|
|
138
|
+
// Do not await any native prompt in setup: plugin activation itself is lazy.
|
|
139
|
+
return async () => {
|
|
140
|
+
lifetime.abort();
|
|
141
|
+
changes.presence.close();
|
|
142
|
+
const tasks = [presenceLoop, eventLoop, coordinating, ...[...receivers.keys()].map(stop)];
|
|
143
|
+
let timer;
|
|
34
144
|
try {
|
|
35
|
-
|
|
36
|
-
if (joined) {
|
|
37
|
-
joining.delete(input.sessionID);
|
|
38
|
-
output.output = `${output.output}${output.output === '' ? '' : '\n\n'}${joined}`;
|
|
39
|
-
}
|
|
40
|
-
await presentPendingAtBoundary(input.sessionID, (context) => {
|
|
41
|
-
output.output = `${output.output}${output.output === '' ? '' : '\n\n'}${context}`;
|
|
42
|
-
});
|
|
145
|
+
await Promise.race([Promise.allSettled(tasks), new Promise((resolve) => { timer = setTimeout(resolve, 3000); })]);
|
|
43
146
|
}
|
|
44
|
-
|
|
45
|
-
|
|
147
|
+
finally {
|
|
148
|
+
if (timer !== undefined)
|
|
149
|
+
clearTimeout(timer);
|
|
46
150
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
151
|
+
// Instance unload is not session deletion: retain memberships and uncertain correlations.
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type LocalEndpoint } from './local-endpoint.js';
|
|
2
|
+
export interface ClaudeAuthOptions {
|
|
3
|
+
readonly endpoint: LocalEndpoint | string;
|
|
4
|
+
readonly token?: string;
|
|
5
|
+
readonly claudeHome?: string;
|
|
6
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
7
|
+
readonly platform?: NodeJS.Platform;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Explicit token, then the environment token bound to this exact endpoint, then
|
|
11
|
+
* the newest readable peer key for it. Windows requires a token; POSIX does not.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveClaudeToken(options: ClaudeAuthOptions): Promise<string | undefined>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { DeliveryError } from './errors.js';
|
|
6
|
+
import { parseLocalEndpoint } from './local-endpoint.js';
|
|
7
|
+
const TOKEN = /^[0-9a-f]{32}$/;
|
|
8
|
+
const KEY = /^(\d+)\.([0-9a-f]{64})\.key$/;
|
|
9
|
+
function validToken(value) { return typeof value === 'string' && TOKEN.test(value); }
|
|
10
|
+
/**
|
|
11
|
+
* Explicit token, then the environment token bound to this exact endpoint, then
|
|
12
|
+
* the newest readable peer key for it. Windows requires a token; POSIX does not.
|
|
13
|
+
*/
|
|
14
|
+
export async function resolveClaudeToken(options) {
|
|
15
|
+
const endpoint = typeof options.endpoint === 'string' ? parseLocalEndpoint(options.endpoint) : options.endpoint;
|
|
16
|
+
const platform = options.platform ?? process.platform;
|
|
17
|
+
const env = options.env ?? process.env;
|
|
18
|
+
if (options.token !== undefined) {
|
|
19
|
+
if (!validToken(options.token))
|
|
20
|
+
throw new DeliveryError('authentication_failed', false);
|
|
21
|
+
return options.token;
|
|
22
|
+
}
|
|
23
|
+
if (validToken(env.CLAUDE_CODE_MESSAGING_TOKEN) && env.CLAUDE_CODE_MESSAGING_SOCKET !== undefined) {
|
|
24
|
+
try {
|
|
25
|
+
if (parseLocalEndpoint(env.CLAUDE_CODE_MESSAGING_SOCKET).canonical === endpoint.canonical)
|
|
26
|
+
return env.CLAUDE_CODE_MESSAGING_TOKEN;
|
|
27
|
+
}
|
|
28
|
+
catch { /* an unrelated malformed environment coordinate is ignored */ }
|
|
29
|
+
}
|
|
30
|
+
const home = options.claudeHome ?? env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude');
|
|
31
|
+
const digest = createHash('sha256').update(endpoint.canonical).digest('hex');
|
|
32
|
+
const keys = [];
|
|
33
|
+
try {
|
|
34
|
+
for (const name of await readdir(join(home, 'sessions'))) {
|
|
35
|
+
const match = KEY.exec(name);
|
|
36
|
+
if (!match || match[2] !== digest)
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
keys.push({ name, pid: Number(match[1]), mtimeMs: (await stat(join(home, 'sessions', name))).mtimeMs });
|
|
40
|
+
}
|
|
41
|
+
catch { /* unreadable entries are skipped */ }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch { /* no registry is not an error; it only means no peer key */ }
|
|
45
|
+
keys.sort((a, b) => Number(isAlive(b.pid)) - Number(isAlive(a.pid)) || b.mtimeMs - a.mtimeMs);
|
|
46
|
+
for (const key of keys) {
|
|
47
|
+
try {
|
|
48
|
+
const value = JSON.parse(await readFile(join(home, 'sessions', key.name), 'utf8'));
|
|
49
|
+
if (value && typeof value === 'object' && validToken(value.peerToken)) {
|
|
50
|
+
return value.peerToken;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch { /* the next readable matching key wins */ }
|
|
54
|
+
}
|
|
55
|
+
if (platform === 'win32')
|
|
56
|
+
throw new DeliveryError('authentication_failed', false);
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
/** A pid that exists, including one owned by another user, holds a live inbox. */
|
|
60
|
+
function isAlive(pid) {
|
|
61
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
62
|
+
return false;
|
|
63
|
+
try {
|
|
64
|
+
process.kill(pid, 0);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
return error.code === 'EPERM';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Plain native coordinate; no Square facts, credentials or claimed authority. */
|
|
2
|
+
export interface ClaudeNativeTarget {
|
|
3
|
+
readonly sessionId: string;
|
|
4
|
+
readonly endpoint: string;
|
|
5
|
+
readonly token?: string;
|
|
6
|
+
readonly claudeHome?: string;
|
|
7
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
8
|
+
readonly platform?: NodeJS.Platform;
|
|
9
|
+
readonly messageId?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ClaudeNativeControl {
|
|
12
|
+
readonly deadline: number;
|
|
13
|
+
readonly signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
export type NativeWriteResult = {
|
|
16
|
+
readonly outcome: 'written';
|
|
17
|
+
readonly message?: never;
|
|
18
|
+
} | {
|
|
19
|
+
readonly outcome: 'unknown';
|
|
20
|
+
readonly code: 'aborted' | 'timeout' | 'transport';
|
|
21
|
+
readonly message: string;
|
|
22
|
+
} | {
|
|
23
|
+
readonly outcome: 'unavailable';
|
|
24
|
+
readonly code: 'aborted' | 'timeout' | 'endpoint_unavailable' | 'invalid_arguments' | 'authentication_failed';
|
|
25
|
+
readonly message: string;
|
|
26
|
+
};
|
|
27
|
+
/** Submit once. Written means local bytes only, never native admission or consumption. */
|
|
28
|
+
export declare function writeClaudeNative(target: ClaudeNativeTarget, text: string, control: ClaudeNativeControl): Promise<NativeWriteResult>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { deadline } from './deadline.js';
|
|
3
|
+
import { connectLocal, LocalEndpointError, parseLocalEndpoint, writeFrames } from './local-endpoint.js';
|
|
4
|
+
import { resolveClaudeToken } from './claude-auth.js';
|
|
5
|
+
const detail = (code) => code === 'aborted' ? 'Native inbox write aborted.'
|
|
6
|
+
: code === 'timeout' ? 'Native inbox write deadline elapsed.'
|
|
7
|
+
: code === 'authentication_failed' ? 'Claude authentication failed.'
|
|
8
|
+
: 'Native inbox endpoint unavailable.';
|
|
9
|
+
/** Submit once. Written means local bytes only, never native admission or consumption. */
|
|
10
|
+
export async function writeClaudeNative(target, text, control) {
|
|
11
|
+
const remaining = control?.deadline - Date.now();
|
|
12
|
+
let endpoint;
|
|
13
|
+
try {
|
|
14
|
+
endpoint = parseLocalEndpoint(target?.endpoint);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return { outcome: 'unavailable', code: 'invalid_arguments', message: 'Invalid native inbox arguments.' };
|
|
18
|
+
}
|
|
19
|
+
if (!target || typeof target.sessionId !== 'string' || !target.sessionId.trim() ||
|
|
20
|
+
typeof text !== 'string' || !text.length || !control ||
|
|
21
|
+
!Number.isFinite(control.deadline) || control.deadline <= 0 || remaining > 2_147_483_647 ||
|
|
22
|
+
(control.signal !== undefined && !(control.signal instanceof AbortSignal))) {
|
|
23
|
+
return { outcome: 'unavailable', code: 'invalid_arguments', message: 'Invalid native inbox arguments.' };
|
|
24
|
+
}
|
|
25
|
+
if (control.signal?.aborted)
|
|
26
|
+
return { outcome: 'unavailable', code: 'aborted', message: detail('aborted') };
|
|
27
|
+
if (remaining <= 0)
|
|
28
|
+
return { outcome: 'unavailable', code: 'timeout', message: detail('timeout') };
|
|
29
|
+
let token;
|
|
30
|
+
try {
|
|
31
|
+
token = await resolveClaudeToken({ endpoint, token: target.token, claudeHome: target.claudeHome, env: target.env, platform: target.platform });
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return { outcome: 'unavailable', code: 'authentication_failed', message: detail('authentication_failed') };
|
|
35
|
+
}
|
|
36
|
+
const frames = [
|
|
37
|
+
...(token === undefined ? [] : [{ type: 'auth', token }]),
|
|
38
|
+
{ msgV: 1, msg_id: target.messageId ?? randomUUID(), type: 'user', session_id: target.sessionId, message: { role: 'user', content: text }, priority: 'next' },
|
|
39
|
+
];
|
|
40
|
+
const scope = deadline(Math.min(remaining, 2_147_483_647), control.signal);
|
|
41
|
+
let socket;
|
|
42
|
+
try {
|
|
43
|
+
scope.check();
|
|
44
|
+
socket = await connectLocal(endpoint, { signal: scope.signal, timeoutMs: Math.max(1, remaining) });
|
|
45
|
+
scope.check();
|
|
46
|
+
// A receiver that never drains must not hold the caller past its deadline.
|
|
47
|
+
await scope.wait(writeFrames(socket, frames));
|
|
48
|
+
scope.check();
|
|
49
|
+
return { outcome: 'written' };
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const local = error instanceof LocalEndpointError ? error : undefined;
|
|
53
|
+
const code = scope.code ?? local?.code;
|
|
54
|
+
if (code === 'aborted' || code === 'timeout') {
|
|
55
|
+
return { outcome: socket ? 'unknown' : 'unavailable', code, message: detail(code) };
|
|
56
|
+
}
|
|
57
|
+
if (socket)
|
|
58
|
+
return { outcome: 'unknown', code: 'transport', message: 'Native inbox write unconfirmed.' };
|
|
59
|
+
return { outcome: 'unavailable', code: 'endpoint_unavailable', message: detail('unavailable') };
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
socket?.destroy();
|
|
63
|
+
scope.close();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ClaudeDeliveryResult, ClaudeSendTextOptions, ClaudeTarget } from './types.js';
|
|
2
|
+
export interface ClaudeConnectExistingOptions {
|
|
3
|
+
readonly harness: 'claude';
|
|
4
|
+
readonly sessionId: string;
|
|
5
|
+
readonly endpoint: string;
|
|
6
|
+
readonly token?: string;
|
|
7
|
+
readonly claudeHome?: string;
|
|
8
|
+
readonly timeoutMs?: number;
|
|
9
|
+
readonly signal?: AbortSignal;
|
|
10
|
+
}
|
|
11
|
+
/** Resolve an explicitly identified existing Claude native inbox; never discovers or starts one. */
|
|
12
|
+
export declare function connectExisting(options: ClaudeConnectExistingOptions): Promise<ClaudeTarget>;
|
|
13
|
+
/** Write once to Claude's next native boundary; local write is never native admission. */
|
|
14
|
+
export declare function sendText(target: ClaudeTarget, text: string, options?: ClaudeSendTextOptions): Promise<ClaudeDeliveryResult>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { deadline, validSignal, validTimeout } from './deadline.js';
|
|
3
|
+
import { ConnectionError, DeliveryError } from './errors.js';
|
|
4
|
+
import { isIdentity, isRecord } from './guards.js';
|
|
5
|
+
import { parseLocalEndpoint } from './local-endpoint.js';
|
|
6
|
+
import { writeClaudeNative } from './claude-native.js';
|
|
7
|
+
import { resolveClaudeToken } from './claude-auth.js';
|
|
8
|
+
const coordinates = new WeakMap();
|
|
9
|
+
/** Resolve an explicitly identified existing Claude native inbox; never discovers or starts one. */
|
|
10
|
+
export async function connectExisting(options) {
|
|
11
|
+
if (!isRecord(options) || options.harness !== 'claude' || !isIdentity(options.sessionId) ||
|
|
12
|
+
typeof options.endpoint !== 'string' || options.endpoint.includes('\0') ||
|
|
13
|
+
!validTimeout(options.timeoutMs) || !validSignal(options.signal))
|
|
14
|
+
throw new ConnectionError('invalid_arguments');
|
|
15
|
+
let endpoint;
|
|
16
|
+
try {
|
|
17
|
+
endpoint = parseLocalEndpoint(options.endpoint);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new ConnectionError('invalid_arguments');
|
|
21
|
+
}
|
|
22
|
+
if (!(process.platform === 'darwin' || process.platform === 'linux' || process.platform === 'win32'))
|
|
23
|
+
throw new ConnectionError('unsupported_platform');
|
|
24
|
+
if (process.platform === 'win32' ? endpoint.kind !== 'pipe' : endpoint.kind !== 'unix')
|
|
25
|
+
throw new ConnectionError('invalid_arguments');
|
|
26
|
+
const scope = deadline(options.timeoutMs, options.signal);
|
|
27
|
+
try {
|
|
28
|
+
scope.check();
|
|
29
|
+
// The ambient environment and platform are the only auth coordinates here.
|
|
30
|
+
const token = await resolveClaudeToken({ endpoint, token: options.token, claudeHome: options.claudeHome });
|
|
31
|
+
scope.check();
|
|
32
|
+
const target = Object.freeze({ harness: 'claude', sessionId: options.sessionId });
|
|
33
|
+
coordinates.set(target, { endpoint: options.endpoint, token, claudeHome: options.claudeHome });
|
|
34
|
+
return target;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (scope.code !== undefined)
|
|
38
|
+
throw new ConnectionError(scope.code);
|
|
39
|
+
if (error instanceof ConnectionError)
|
|
40
|
+
throw error;
|
|
41
|
+
if (error instanceof DeliveryError)
|
|
42
|
+
throw new ConnectionError(error.code);
|
|
43
|
+
throw new ConnectionError('invalid_response');
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
scope.close();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Write once to Claude's next native boundary; local write is never native admission. */
|
|
50
|
+
export async function sendText(target, text, options = {}) {
|
|
51
|
+
const coordinate = isRecord(target) ? coordinates.get(target) : undefined;
|
|
52
|
+
if (!coordinate || target.harness !== 'claude' || !isIdentity(target.sessionId) || typeof text !== 'string' || !text.length || !isRecord(options) ||
|
|
53
|
+
(options.delivery !== undefined && options.delivery !== 'steer') || options.inputId !== undefined ||
|
|
54
|
+
!validTimeout(options.timeoutMs) || !validSignal(options.signal))
|
|
55
|
+
throw new TypeError('Invalid Claude sendText arguments.');
|
|
56
|
+
const messageId = randomUUID();
|
|
57
|
+
const result = await writeClaudeNative({ ...coordinate, sessionId: target.sessionId, messageId }, text, { deadline: Date.now() + (options.timeoutMs ?? 5_000), signal: options.signal });
|
|
58
|
+
const attempt = { harness: 'claude', sessionId: target.sessionId };
|
|
59
|
+
if (result.outcome === 'written')
|
|
60
|
+
return { ...attempt, state: 'written', messageId };
|
|
61
|
+
if (result.outcome === 'unknown')
|
|
62
|
+
return { ...attempt, state: 'unknown', code: result.code };
|
|
63
|
+
if (result.code === 'invalid_arguments')
|
|
64
|
+
throw new TypeError('Invalid Claude sendText arguments.');
|
|
65
|
+
return { ...attempt, state: 'unavailable', code: result.code };
|
|
66
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const DEFAULT_TIMEOUT_MS = 5000;
|
|
2
|
+
export declare function validTimeout(value: unknown): value is number | undefined;
|
|
3
|
+
export declare function validSignal(value: unknown): value is AbortSignal | undefined;
|
|
4
|
+
/** Bounds caller waiting even for Service.discover, which cannot accept a signal. */
|
|
5
|
+
export declare function deadline(timeoutMs?: number, parent?: AbortSignal): {
|
|
6
|
+
signal: AbortSignal;
|
|
7
|
+
readonly code: "aborted" | "timeout" | undefined;
|
|
8
|
+
check(): void;
|
|
9
|
+
wait<T>(operation: Promise<T>): Promise<T>;
|
|
10
|
+
close(): void;
|
|
11
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const DEFAULT_TIMEOUT_MS = 5_000;
|
|
2
|
+
export function validTimeout(value) {
|
|
3
|
+
// Node clamps larger timers to 1ms; do not silently turn a deadline into that.
|
|
4
|
+
return value === undefined ||
|
|
5
|
+
(typeof value === 'number' && Number.isFinite(value) && value > 0 && value <= 2_147_483_647);
|
|
6
|
+
}
|
|
7
|
+
export function validSignal(value) {
|
|
8
|
+
return value === undefined || value instanceof AbortSignal;
|
|
9
|
+
}
|
|
10
|
+
/** Bounds caller waiting even for Service.discover, which cannot accept a signal. */
|
|
11
|
+
export function deadline(timeoutMs = DEFAULT_TIMEOUT_MS, parent) {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
let code;
|
|
14
|
+
let rejectStopped;
|
|
15
|
+
const stopped = new Promise((_, reject) => { rejectStopped = reject; });
|
|
16
|
+
// A pre-aborted scope may never reach wait().
|
|
17
|
+
void stopped.catch(() => { });
|
|
18
|
+
const stop = (reason) => {
|
|
19
|
+
if (code !== undefined)
|
|
20
|
+
return;
|
|
21
|
+
code = reason;
|
|
22
|
+
controller.abort();
|
|
23
|
+
rejectStopped(new Error('Operation stopped.'));
|
|
24
|
+
};
|
|
25
|
+
const onAbort = () => stop('aborted');
|
|
26
|
+
parent?.addEventListener('abort', onAbort, { once: true });
|
|
27
|
+
if (parent?.aborted)
|
|
28
|
+
onAbort();
|
|
29
|
+
const timer = setTimeout(() => stop('timeout'), timeoutMs);
|
|
30
|
+
return {
|
|
31
|
+
signal: controller.signal,
|
|
32
|
+
get code() { return code; },
|
|
33
|
+
check() { if (code !== undefined)
|
|
34
|
+
throw new Error('Operation stopped.'); },
|
|
35
|
+
wait(operation) { return Promise.race([operation, stopped]); },
|
|
36
|
+
close() {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
parent?.removeEventListener('abort', onAbort);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type DeliveryErrorCode = 'invalid_arguments' | 'unsupported_platform' | 'unavailable' | 'authentication_failed' | 'session_not_found' | 'rejected' | 'unsupported_version' | 'invalid_response' | 'timeout' | 'aborted' | 'transport';
|
|
2
|
+
/** Diagnostics intentionally contain no endpoint, credentials, body, or SDK cause. */
|
|
3
|
+
export declare class DeliveryError extends Error {
|
|
4
|
+
readonly code: DeliveryErrorCode;
|
|
5
|
+
readonly maybeDelivered: boolean;
|
|
6
|
+
readonly id?: string;
|
|
7
|
+
readonly status?: number;
|
|
8
|
+
constructor(code: DeliveryErrorCode, maybeDelivered?: boolean, id?: string, status?: number);
|
|
9
|
+
}
|
|
10
|
+
/** Internal connection precondition failure; the public entry translates it. */
|
|
11
|
+
export type ConnectionErrorCode = DeliveryErrorCode | 'service_unavailable' | 'http_rejection';
|
|
12
|
+
export declare class ConnectionError extends Error {
|
|
13
|
+
readonly code: ConnectionErrorCode;
|
|
14
|
+
readonly status?: number;
|
|
15
|
+
constructor(code: ConnectionErrorCode, status?: number);
|
|
16
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const messages = {
|
|
2
|
+
invalid_arguments: 'Invalid delivery arguments.',
|
|
3
|
+
unsupported_platform: 'This delivery transport is not supported on this platform.',
|
|
4
|
+
unavailable: 'The delivery endpoint is unavailable.',
|
|
5
|
+
authentication_failed: 'Delivery authentication failed.',
|
|
6
|
+
session_not_found: 'The requested session was not found.',
|
|
7
|
+
rejected: 'The delivery was rejected.',
|
|
8
|
+
unsupported_version: 'The receiving service version is unsupported.',
|
|
9
|
+
invalid_response: 'The receiving service returned an invalid response.',
|
|
10
|
+
timeout: 'The delivery deadline expired.',
|
|
11
|
+
aborted: 'The delivery was aborted.',
|
|
12
|
+
transport: 'The delivery transport failed.',
|
|
13
|
+
};
|
|
14
|
+
/** Diagnostics intentionally contain no endpoint, credentials, body, or SDK cause. */
|
|
15
|
+
export class DeliveryError extends Error {
|
|
16
|
+
code;
|
|
17
|
+
maybeDelivered;
|
|
18
|
+
id;
|
|
19
|
+
status;
|
|
20
|
+
constructor(code, maybeDelivered = false, id, status) {
|
|
21
|
+
super(messages[code]);
|
|
22
|
+
this.name = 'DeliveryError';
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.maybeDelivered = maybeDelivered;
|
|
25
|
+
if (id !== undefined)
|
|
26
|
+
this.id = id;
|
|
27
|
+
if (status !== undefined && Number.isInteger(status) && status >= 100 && status <= 599)
|
|
28
|
+
this.status = status;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export class ConnectionError extends Error {
|
|
32
|
+
code;
|
|
33
|
+
status;
|
|
34
|
+
constructor(code, status) {
|
|
35
|
+
super(messages[code] ?? (code === 'http_rejection' ? 'The receiving service rejected the request.' : 'No compatible existing service is available.'));
|
|
36
|
+
this.name = 'ConnectionError';
|
|
37
|
+
this.code = code;
|
|
38
|
+
if (status !== undefined && Number.isInteger(status) && status >= 100 && status <= 599)
|
|
39
|
+
this.status = status;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two structure checks every harness front door repeats: a plain object and a
|
|
3
|
+
* non-empty identity string. They live here once instead of once per transport.
|
|
4
|
+
*/
|
|
5
|
+
export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
|
|
6
|
+
export declare const isIdentity: (value: unknown) => value is string;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two structure checks every harness front door repeats: a plain object and a
|
|
3
|
+
* non-empty identity string. They live here once instead of once per transport.
|
|
4
|
+
*/
|
|
5
|
+
export const isRecord = (value) => typeof value === 'object' && value !== null;
|
|
6
|
+
export const isIdentity = (value) => typeof value === 'string' && value.trim().length > 0;
|