@astrosheep/square 0.4.1 → 0.4.3
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/dist/claude-boundary-state.d.ts +20 -0
- package/dist/claude-boundary-state.js +84 -0
- package/dist/claude-hook.d.ts +2 -0
- package/dist/claude-hook.js +231 -0
- package/dist/claude-mcp-context.d.ts +1 -1
- package/dist/claude-mcp-context.js +10 -1
- package/dist/cli/observation-commands.d.ts +2 -1
- package/dist/cli/observation-commands.js +25 -2
- package/dist/cli/registry.js +3 -2
- package/dist/delivery-ledger-sqlite.d.ts +2 -0
- package/dist/delivery-ledger-sqlite.js +21 -1
- package/dist/harness-claude.js +3 -4
- package/dist/harness-links.js +4 -2
- package/dist/help.js +2 -1
- package/dist/mcp.js +1 -2
- package/dist/notifications.js +3 -3
- package/dist/plugins/opencode-v1/index.js +1 -1
- package/dist/wake-transports.d.ts +1 -1
- package/dist/wake-transports.js +2 -3
- package/guides/claude-native.md +9 -17
- package/guides/mcp.md +1 -1
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +2 -3
- package/plugins/claude/hooks/hooks.json +11 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/opencode-v1/index.ts +1 -1
- package/dist/claude-delivery.d.ts +0 -17
- package/dist/claude-delivery.js +0 -55
- package/dist/claude-mod.d.ts +0 -8
- package/dist/claude-mod.js +0 -60
- package/dist/claude-transport.d.ts +0 -5
- package/dist/claude-transport.js +0 -34
- package/plugins/claude/mods/square.js +0 -129
- package/plugins/claude/types.d.ts +0 -7
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
declare const stateSchema: z.ZodObject<{
|
|
3
|
+
generation: z.ZodString;
|
|
4
|
+
promptId: z.ZodOptional<z.ZodString>;
|
|
5
|
+
stopEpoch: z.ZodString;
|
|
6
|
+
consumed: z.ZodBoolean;
|
|
7
|
+
ended: z.ZodBoolean;
|
|
8
|
+
}, z.core.$strip>;
|
|
9
|
+
export type ClaudeBoundary = z.infer<typeof stateSchema>;
|
|
10
|
+
/** Receiver operational facts only: no cards, claims, or payload copies. */
|
|
11
|
+
export declare function claudeBoundaryStore(session: string, env: NodeJS.ProcessEnv): {
|
|
12
|
+
start(attach: () => Promise<string | false>): Promise<void>;
|
|
13
|
+
end(): Promise<ClaudeBoundary | undefined>;
|
|
14
|
+
prompt(promptId: string | undefined): Promise<void>;
|
|
15
|
+
capture(promptId: string): Promise<ClaudeBoundary | undefined>;
|
|
16
|
+
current(captured: ClaudeBoundary): Promise<boolean>;
|
|
17
|
+
consume(captured: ClaudeBoundary): Promise<void>;
|
|
18
|
+
watch<T>(signal: AbortSignal, run: () => Promise<T>): Promise<T>;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { z } from 'zod/v4';
|
|
5
|
+
import { deliveryLedgerLocation } from './delivery-ledger.js';
|
|
6
|
+
import { withFileLock } from './file-lock.js';
|
|
7
|
+
const stateSchema = z.object({
|
|
8
|
+
generation: z.string().min(1), promptId: z.string().min(1).optional(),
|
|
9
|
+
stopEpoch: z.string().min(1), consumed: z.boolean(), ended: z.boolean(),
|
|
10
|
+
});
|
|
11
|
+
/** Receiver operational facts only: no cards, claims, or payload copies. */
|
|
12
|
+
export function claudeBoundaryStore(session, env) {
|
|
13
|
+
const root = path.join(path.dirname(deliveryLedgerLocation(env)), 'claude-hooks');
|
|
14
|
+
const key = createHash('sha256').update(session).digest('hex');
|
|
15
|
+
const file = path.join(root, `${key}.json`);
|
|
16
|
+
async function mutate(change) {
|
|
17
|
+
await fs.mkdir(root, { recursive: true, mode: 0o700 });
|
|
18
|
+
await fs.chmod(root, 0o700);
|
|
19
|
+
return withFileLock(path.join(root, `${key}.lock`), { retryMs: 10, signal: AbortSignal.timeout(2500) }, async () => {
|
|
20
|
+
let state;
|
|
21
|
+
try {
|
|
22
|
+
state = stateSchema.parse(JSON.parse(await fs.readFile(file, 'utf8')));
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (error.code !== 'ENOENT')
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
return change(state, async (next) => {
|
|
29
|
+
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
30
|
+
try {
|
|
31
|
+
await fs.writeFile(temporary, JSON.stringify(next), { mode: 0o600, flag: 'wx' });
|
|
32
|
+
await fs.rename(temporary, file);
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await fs.rm(temporary, { force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const matches = (state, captured) => !!state && !state.ended && !state.consumed
|
|
41
|
+
&& state.generation === captured.generation && state.promptId === captured.promptId && state.stopEpoch === captured.stopEpoch;
|
|
42
|
+
return {
|
|
43
|
+
async start(attach) {
|
|
44
|
+
await mutate(async (state, save) => {
|
|
45
|
+
if (state)
|
|
46
|
+
await save({ ...state, promptId: undefined, ended: true, stopEpoch: randomUUID() });
|
|
47
|
+
const generation = await attach();
|
|
48
|
+
if (generation)
|
|
49
|
+
await save({ generation, stopEpoch: randomUUID(), consumed: false, ended: false });
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
async end() {
|
|
53
|
+
return mutate(async (state, save) => {
|
|
54
|
+
if (state)
|
|
55
|
+
await save({ ...state, ended: true, stopEpoch: randomUUID() });
|
|
56
|
+
return state;
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
async prompt(promptId) {
|
|
60
|
+
await mutate(async (state, save) => {
|
|
61
|
+
if (state && !state.ended)
|
|
62
|
+
await save({ ...state, promptId, stopEpoch: randomUUID(), consumed: false });
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
capture(promptId) {
|
|
66
|
+
return mutate(async (state) => state && !state.ended && !state.consumed && state.promptId === promptId ? state : undefined);
|
|
67
|
+
},
|
|
68
|
+
current(captured) { return mutate(async (state) => matches(state, captured)); },
|
|
69
|
+
async consume(captured) {
|
|
70
|
+
await mutate(async (state, save) => {
|
|
71
|
+
if (matches(state, captured))
|
|
72
|
+
await save({ ...state, consumed: true });
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
async watch(signal, run) {
|
|
76
|
+
await fs.mkdir(root, { recursive: true, mode: 0o700 });
|
|
77
|
+
const lock = path.join(root, `${key}.watch.lock`);
|
|
78
|
+
const handle = await fs.open(lock, 'a', 0o600);
|
|
79
|
+
await handle.close();
|
|
80
|
+
// A newer Stop waits through bounded shutdown; it must not skip a busy lock.
|
|
81
|
+
return withFileLock(lock, { retryMs: 10, signal }, run);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare function runClaudeHook(text: string, env?: NodeJS.ProcessEnv, sink?: (value: string) => void | Promise<void>): Promise<string>;
|
|
2
|
+
export declare function runClaudeWatch(text: string, env?: NodeJS.ProcessEnv, signal?: AbortSignal, output?: (frame: string) => Promise<void>): Promise<string>;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { DeliveryLedger, deliveryLedgerLocation } from './delivery-ledger.js';
|
|
3
|
+
import { claudeBoundaryStore } from './claude-boundary-state.js';
|
|
4
|
+
import { harnessOwner } from './owner.js';
|
|
5
|
+
import { attachDeliverySessionRoutes, detachDeliverySession } from './session-lifecycle.js';
|
|
6
|
+
import { presentPendingAtBoundary } from './boundary-presentation.js';
|
|
7
|
+
import { renderDeliveryReminder } from './delivery-frame.js';
|
|
8
|
+
import { beginClaudeMcpCall, endClaudeMcpCallForSession } from './claude-mcp-context.js';
|
|
9
|
+
import { retainDeliveryLedger } from './delivery-lifetime.js';
|
|
10
|
+
import { z } from 'zod/v4';
|
|
11
|
+
const nativeId = z.string().min(1).max(4096).regex(/^[^\x00-\x1f\x7f]+$/u);
|
|
12
|
+
const hookInput = z.object({
|
|
13
|
+
hook_event_name: nativeId, session_id: nativeId, cwd: z.string().optional(),
|
|
14
|
+
prompt_id: nativeId.optional().catch(undefined), prompt: z.string().optional(),
|
|
15
|
+
tool_use_id: nativeId.optional().catch(undefined), tool_name: z.string().optional(), agent_id: nativeId.optional(),
|
|
16
|
+
});
|
|
17
|
+
const route = 'claude-async-hook';
|
|
18
|
+
const ownerFor = (input) => harnessOwner({ channel: 'claude-code', session: input.session_id });
|
|
19
|
+
function parseInput(text) {
|
|
20
|
+
try {
|
|
21
|
+
const value = hookInput.safeParse(JSON.parse(text));
|
|
22
|
+
return value.success ? value.data : undefined;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function observedFrames(prompt) {
|
|
29
|
+
const prefix = renderDeliveryReminder([''], { tagged: false });
|
|
30
|
+
const result = [];
|
|
31
|
+
for (const match of prompt.matchAll(/\[square-delivery:([a-f0-9-]+)\]\n[\s\S]*?\n\[\/square-delivery:\1\]/g)) {
|
|
32
|
+
const start = match.index - prefix.length;
|
|
33
|
+
if (start < 0 || prompt.slice(start, match.index) !== prefix)
|
|
34
|
+
continue;
|
|
35
|
+
result.push({ token: match[1], payloadHash: createHash('sha256').update(prefix + match[0]).digest('hex') });
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
function claudeFrame(card, token) {
|
|
40
|
+
return renderDeliveryReminder([`[square-delivery:${token}]\n${card.text}\n[/square-delivery:${token}]`], { tagged: false });
|
|
41
|
+
}
|
|
42
|
+
export async function runClaudeHook(text, env = process.env, sink = value => { process.stdout.write(value); }) {
|
|
43
|
+
const input = parseInput(text);
|
|
44
|
+
if (!input) {
|
|
45
|
+
let raw;
|
|
46
|
+
try {
|
|
47
|
+
raw = JSON.parse(text);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
if (raw?.hook_event_name === 'PreToolUse' && typeof raw.tool_name === 'string' && raw.tool_name.startsWith('mcp__plugin_square_square__'))
|
|
53
|
+
await denyMcpTool(sink);
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
const boundaries = claudeBoundaryStore(input.session_id, env);
|
|
57
|
+
const mainOwner = ownerFor(input);
|
|
58
|
+
const owner = harnessOwner({ channel: 'claude-code', session: input.session_id, ...(input.agent_id === undefined ? {} : { agent: input.agent_id }) });
|
|
59
|
+
if (input.hook_event_name === 'SessionStart') {
|
|
60
|
+
await boundaries.start(async () => {
|
|
61
|
+
const session = await attachDeliverySessionRoutes(mainOwner, env, [{ kind: route, address: { sessionId: input.session_id } }], AbortSignal.timeout(4500));
|
|
62
|
+
if (!session)
|
|
63
|
+
return false;
|
|
64
|
+
const ledger = await DeliveryLedger.open({ location: deliveryLedgerLocation(env) });
|
|
65
|
+
try {
|
|
66
|
+
await ledger.updateRoutes({ owner: mainOwner, generation: session.generation, routes: session.routes.filter(item => item.kind !== 'claude-native') });
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await ledger.close();
|
|
70
|
+
}
|
|
71
|
+
return session.generation;
|
|
72
|
+
});
|
|
73
|
+
return '';
|
|
74
|
+
}
|
|
75
|
+
if (input.hook_event_name === 'SessionEnd') {
|
|
76
|
+
const captured = await boundaries.end();
|
|
77
|
+
if (captured)
|
|
78
|
+
await detachDeliverySession(mainOwner, captured.generation, env);
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
if (input.hook_event_name === 'UserPromptSubmit') {
|
|
82
|
+
await boundaries.prompt(input.prompt_id);
|
|
83
|
+
if (!input.prompt_id)
|
|
84
|
+
return '';
|
|
85
|
+
if (input.prompt) {
|
|
86
|
+
const ledger = await DeliveryLedger.open({ location: deliveryLedgerLocation(env) });
|
|
87
|
+
try {
|
|
88
|
+
for (const frame of observedFrames(input.prompt)) {
|
|
89
|
+
const handoff = await ledger.handoff(frame.token);
|
|
90
|
+
if (!handoff || handoff.owner !== owner || handoff.routeKind !== route || handoff.payloadHash !== frame.payloadHash)
|
|
91
|
+
continue;
|
|
92
|
+
await ledger.observePresentation(handoff);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
await ledger.close();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
await presentPendingAtBoundary(owner, async (context) => {
|
|
100
|
+
await sink(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context } })}\n`);
|
|
101
|
+
}, env, AbortSignal.timeout(4500));
|
|
102
|
+
return '';
|
|
103
|
+
}
|
|
104
|
+
if (input.hook_event_name === 'PreToolUse' && input.tool_name?.startsWith('mcp__plugin_square_square__')) {
|
|
105
|
+
if (!input.tool_use_id || !input.cwd) {
|
|
106
|
+
await denyMcpTool(sink);
|
|
107
|
+
return '';
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
await beginClaudeMcpCall({ toolUseId: input.tool_use_id, sessionId: input.session_id, cwd: input.cwd, agentId: input.agent_id }, env);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
await denyMcpTool(sink);
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if ((input.hook_event_name === 'PostToolUse' || input.hook_event_name === 'PostToolUseFailure')
|
|
118
|
+
&& input.tool_name?.startsWith('mcp__plugin_square_square__') && input.tool_use_id) {
|
|
119
|
+
await endClaudeMcpCallForSession(input.tool_use_id, input.session_id, env, input.agent_id).catch(() => undefined);
|
|
120
|
+
}
|
|
121
|
+
if (['PreToolUse', 'PostToolUse', 'PostToolUseFailure'].includes(input.hook_event_name)) {
|
|
122
|
+
await presentPendingAtBoundary(owner, async (context) => {
|
|
123
|
+
await sink(`${JSON.stringify({ hookSpecificOutput: { hookEventName: input.hook_event_name, additionalContext: context } })}\n`);
|
|
124
|
+
}, env, AbortSignal.timeout(4500));
|
|
125
|
+
}
|
|
126
|
+
return '';
|
|
127
|
+
}
|
|
128
|
+
async function denyMcpTool(sink) {
|
|
129
|
+
try {
|
|
130
|
+
await sink(`${JSON.stringify({ hookSpecificOutput: {
|
|
131
|
+
hookEventName: 'PreToolUse', permissionDecision: 'deny',
|
|
132
|
+
permissionDecisionReason: 'Square MCP call identity is unavailable; this call did not run.',
|
|
133
|
+
} })}\n`);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
process.exitCode = 2;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export async function runClaudeWatch(text, env = process.env, signal, output = frame => new Promise((resolve, reject) => {
|
|
140
|
+
process.stderr.write(frame, error => error ? reject(error) : resolve());
|
|
141
|
+
})) {
|
|
142
|
+
const input = parseInput(text);
|
|
143
|
+
if (!input || input.hook_event_name !== 'Stop' || !input.prompt_id)
|
|
144
|
+
return '';
|
|
145
|
+
const owner = ownerFor(input);
|
|
146
|
+
const boundaries = claudeBoundaryStore(input.session_id, env);
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
const timer = setTimeout(() => controller.abort(), 86_400_000);
|
|
149
|
+
const stop = () => controller.abort();
|
|
150
|
+
process.once('SIGTERM', stop);
|
|
151
|
+
process.once('SIGINT', stop);
|
|
152
|
+
const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
153
|
+
try {
|
|
154
|
+
const captured = await boundaries.capture(input.prompt_id);
|
|
155
|
+
if (!captured)
|
|
156
|
+
return '';
|
|
157
|
+
await boundaries.watch(combined, async () => {
|
|
158
|
+
if (!await boundaries.current(captured))
|
|
159
|
+
return;
|
|
160
|
+
const ledger = await DeliveryLedger.open({ location: deliveryLedgerLocation(env) });
|
|
161
|
+
const lifetime = retainDeliveryLedger(ledger);
|
|
162
|
+
const observer = await ledger.observe(owner);
|
|
163
|
+
try {
|
|
164
|
+
while (!combined.aborted) {
|
|
165
|
+
let version = await observer.read(combined);
|
|
166
|
+
const live = await boundaries.current(captured);
|
|
167
|
+
const session = await ledger.session(owner);
|
|
168
|
+
if (!live || session?.state !== 'attached' || session.generation !== captured.generation || !session.routes.some(item => item.kind === route))
|
|
169
|
+
return;
|
|
170
|
+
const cards = await ledger.pending(owner, { generation: captured.generation });
|
|
171
|
+
const held = new Set((await ledger.handoffs(owner, { generation: captured.generation }))
|
|
172
|
+
.filter(attempt => !['failed', 'released'].includes(attempt.outcome)).map(attempt => attempt.seq));
|
|
173
|
+
for (const card of cards) {
|
|
174
|
+
if (held.has(card.seq))
|
|
175
|
+
continue;
|
|
176
|
+
const transport = {
|
|
177
|
+
kind: route,
|
|
178
|
+
async send(deliveryCard, address, context) {
|
|
179
|
+
if (address.sessionId !== input.session_id)
|
|
180
|
+
return 'not-capable';
|
|
181
|
+
const frame = claudeFrame(deliveryCard, context.token);
|
|
182
|
+
const payloadHash = createHash('sha256').update(frame).digest('hex');
|
|
183
|
+
if (!await context.prepare({ token: context.token, messageId: context.token, payloadHash }))
|
|
184
|
+
return 'not-capable';
|
|
185
|
+
const currentSession = await ledger.session(owner);
|
|
186
|
+
if (currentSession?.state !== 'attached' || currentSession.generation !== captured.generation
|
|
187
|
+
|| !currentSession.routes.some(item => item.kind === route) || !await context.beforeSend())
|
|
188
|
+
return 'not-capable';
|
|
189
|
+
const native = await boundaries.current(captured);
|
|
190
|
+
if (!native || context.signal.aborted || Date.now() >= context.deadline)
|
|
191
|
+
return 'not-capable';
|
|
192
|
+
try {
|
|
193
|
+
await output(frame);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
await boundaries.consume(captured);
|
|
197
|
+
}
|
|
198
|
+
process.exitCode = 2;
|
|
199
|
+
return 'unknown';
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
const result = await ledger.dispatchHandoff(owner, card.key, transport, {
|
|
203
|
+
generation: captured.generation, timeoutMs: 5000, signal: combined,
|
|
204
|
+
});
|
|
205
|
+
lifetime.track(result);
|
|
206
|
+
if (result.outcome === 'unknown' || result.outcome === 'accepted' || result.outcome === 'presented')
|
|
207
|
+
return;
|
|
208
|
+
// Drain our own unsuccessful claim/settlement edge before waiting;
|
|
209
|
+
// a not-capable route must not make filesystem hints a retry loop.
|
|
210
|
+
version = await observer.read(combined);
|
|
211
|
+
}
|
|
212
|
+
await observer.changed(version, 1000, combined);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
observer.close();
|
|
217
|
+
await lifetime.close();
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
if (!combined.aborted)
|
|
223
|
+
return '';
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
process.off('SIGTERM', stop);
|
|
228
|
+
process.off('SIGINT', stop);
|
|
229
|
+
}
|
|
230
|
+
return '';
|
|
231
|
+
}
|
|
@@ -10,6 +10,6 @@ export declare function beginClaudeMcpCall(input: ClaudeMcpCall, env?: NodeJS.Pr
|
|
|
10
10
|
token: string;
|
|
11
11
|
}>;
|
|
12
12
|
export declare function endClaudeMcpCall(toolUseId: string, token: string, env?: NodeJS.ProcessEnv): Promise<void>;
|
|
13
|
-
|
|
13
|
+
export declare function endClaudeMcpCallForSession(toolUseId: string, sessionId: string, env?: NodeJS.ProcessEnv, agentId?: string): Promise<void>;
|
|
14
14
|
export declare function readClaudeMcpCall(toolUseId: unknown, env?: NodeJS.ProcessEnv): Promise<ClaudeMcpCall>;
|
|
15
15
|
export {};
|
|
@@ -71,7 +71,16 @@ export async function endClaudeMcpCall(toolUseId, token, env = process.env) {
|
|
|
71
71
|
await fs.unlink(file);
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
export async function endClaudeMcpCallForSession(toolUseId, sessionId, env = process.env, agentId) {
|
|
75
|
+
if (!text.safeParse(toolUseId).success || !text.safeParse(sessionId).success)
|
|
76
|
+
throw unavailable();
|
|
77
|
+
await mutate(env, async (root) => {
|
|
78
|
+
const file = callFile(root, toolUseId);
|
|
79
|
+
const stored = await readRecord(file);
|
|
80
|
+
if (stored?.toolUseId === toolUseId && stored.sessionId === sessionId && stored.agentId === agentId)
|
|
81
|
+
await fs.unlink(file);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
75
84
|
export async function readClaudeMcpCall(toolUseId, env = process.env) {
|
|
76
85
|
if (!text.safeParse(toolUseId).success)
|
|
77
86
|
throw unavailable();
|
|
@@ -25,7 +25,8 @@ interface InboxIntent {
|
|
|
25
25
|
limit: number;
|
|
26
26
|
}
|
|
27
27
|
export declare const inboxCommand: CommandSpec<InboxIntent, string>;
|
|
28
|
-
export declare const
|
|
28
|
+
export declare const claudeHookCommand: CommandSpec<undefined, string>;
|
|
29
|
+
export declare const claudeWatchCommand: CommandSpec<undefined, string>;
|
|
29
30
|
export declare const codexHookCommand: CommandSpec<undefined, string>;
|
|
30
31
|
export declare const codexWatchCommand: CommandSpec<undefined, string>;
|
|
31
32
|
export declare const codexInterruptCommand: CommandSpec<undefined, string>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { once } from 'node:events';
|
|
3
|
-
import {
|
|
3
|
+
import { runClaudeHook, runClaudeWatch } from '../claude-hook.js';
|
|
4
4
|
import { runCodexHook, runCodexInterrupt } from '../codex-hook.js';
|
|
5
5
|
import { runCodexWatch } from '../codex-watch.js';
|
|
6
6
|
import { isSquareError } from '../model.js';
|
|
@@ -526,7 +526,30 @@ function hookCommand(runHook) {
|
|
|
526
526
|
present: (result) => process.stdout.write(result),
|
|
527
527
|
};
|
|
528
528
|
}
|
|
529
|
-
export const
|
|
529
|
+
export const claudeHookCommand = {
|
|
530
|
+
parse(argv, context) { if (argv.length > 0)
|
|
531
|
+
usage(context.command); return undefined; },
|
|
532
|
+
execute: async () => {
|
|
533
|
+
try {
|
|
534
|
+
return await runClaudeHook(await readStdin(), process.env, text => new Promise((resolve, reject) => {
|
|
535
|
+
process.stdout.write(text, error => { if (error)
|
|
536
|
+
reject(error);
|
|
537
|
+
else
|
|
538
|
+
resolve(); });
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
541
|
+
catch {
|
|
542
|
+
return '';
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
present: () => { },
|
|
546
|
+
};
|
|
547
|
+
export const claudeWatchCommand = {
|
|
548
|
+
parse(argv, context) { if (argv.length > 0)
|
|
549
|
+
usage(context.command); return undefined; },
|
|
550
|
+
execute: async () => runClaudeWatch(await readStdin(), process.env),
|
|
551
|
+
present: () => { },
|
|
552
|
+
};
|
|
530
553
|
export const codexHookCommand = {
|
|
531
554
|
parse(argv, context) { if (argv.length > 0)
|
|
532
555
|
usage(context.command); return undefined; },
|
package/dist/cli/registry.js
CHANGED
|
@@ -4,7 +4,7 @@ import { harnessCommand, installCommand, uninstallCommand } from './harness-comm
|
|
|
4
4
|
import { helpCommand, versionCommand } from './meta-commands.js';
|
|
5
5
|
import { mcpServerCommand } from './mcp-server-command.js';
|
|
6
6
|
import { JSON_COMMANDS } from './json.js';
|
|
7
|
-
import { catchCommand,
|
|
7
|
+
import { catchCommand, claudeHookCommand, claudeWatchCommand, codexHookCommand, codexWatchCommand, codexInterruptCommand, historyCommand, inboxCommand, listCommand, participantsCommand, statusCommand, streamCommand, } from './observation-commands.js';
|
|
8
8
|
/** Every public command is an executable adapter, including aliases and utility commands. */
|
|
9
9
|
export const commandRegistry = [
|
|
10
10
|
{ names: ['build'], spec: buildCommand },
|
|
@@ -12,7 +12,8 @@ export const commandRegistry = [
|
|
|
12
12
|
{ names: ['join'], spec: joinCommand },
|
|
13
13
|
{ names: ['stream'], spec: streamCommand },
|
|
14
14
|
{ names: ['inbox'], spec: inboxCommand },
|
|
15
|
-
{ names: ['claude-
|
|
15
|
+
{ names: ['claude-hook'], spec: claudeHookCommand },
|
|
16
|
+
{ names: ['claude-watch'], spec: claudeWatchCommand },
|
|
16
17
|
{ names: ['codex-hook'], spec: codexHookCommand },
|
|
17
18
|
{ names: ['codex-watch'], spec: codexWatchCommand },
|
|
18
19
|
{ names: ['codex-interrupt'], spec: codexInterruptCommand },
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { DatabaseSync as NodeDatabaseSync } from 'node:sqlite';
|
|
2
|
+
export declare const DatabaseSync: typeof NodeDatabaseSync;
|
|
1
3
|
import type { DeliveryAttempt, DeliveryCard, DeliveryCorrelation, DeliveryHandoffRef, DeliveryHealth, DeliveryOutcome, DeliveryReceipt, DeliveryRoute, DeliverySession, PostResult, SuppressionResult } from './delivery-ledger.js';
|
|
2
4
|
/** All ledger file access and SQL are confined here. Transactions never span external effects. */
|
|
3
5
|
export declare class LedgerStore {
|
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
|
|
4
|
+
const bunSqlite = 'bun:sqlite';
|
|
5
|
+
export const { DatabaseSync } = await (process.versions.bun
|
|
6
|
+
? import(bunSqlite).then(({ Database }) => ({
|
|
7
|
+
DatabaseSync: class {
|
|
8
|
+
db;
|
|
9
|
+
constructor(location, options) {
|
|
10
|
+
this.db = new Database(location, options?.readOnly ? { readonly: true } : { create: true, readwrite: true });
|
|
11
|
+
}
|
|
12
|
+
exec(sql) { this.db.exec(sql); }
|
|
13
|
+
close() { this.db.close(); }
|
|
14
|
+
prepare(sql) {
|
|
15
|
+
const statement = this.db.prepare(sql);
|
|
16
|
+
return {
|
|
17
|
+
get: (...values) => statement.get(...values) ?? undefined,
|
|
18
|
+
all: (...values) => statement.all(...values),
|
|
19
|
+
run: (...values) => statement.run(...values),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
}))
|
|
24
|
+
: import('node:sqlite'));
|
|
5
25
|
import { inSqliteTransaction } from './sqlite-transaction.js';
|
|
6
26
|
import { validateSchema } from './sqlite-framing.js';
|
|
7
27
|
const FORMAT = 3;
|
package/dist/harness-claude.js
CHANGED
|
@@ -71,9 +71,8 @@ export async function doctorClaudePlugin(homeDir, run = runClaude) {
|
|
|
71
71
|
return [
|
|
72
72
|
fs.existsSync(bundle) ? `✓ Square Claude plugin bundle ${root}` : `○ Square Claude plugin bundle missing ${root}`,
|
|
73
73
|
installed ? `✓ ${CLAUDE_PLUGIN_ID} installed` : `○ ${CLAUDE_PLUGIN_ID} unavailable`,
|
|
74
|
-
`Claude
|
|
75
|
-
`
|
|
76
|
-
`
|
|
77
|
-
`Live routes and admission/presentation evidence: square harness doctor delivery. No legacy fallback.`,
|
|
74
|
+
`Claude delivery uses synchronous prompt/tool hooks and one finite 24-hour asyncRewake Stop observer; actual coverage ends at native boundaries.`,
|
|
75
|
+
`Idle Escape cannot disarm a watcher, and already emitted notifications cannot be recalled; busy Escape disarms at the next prompt boundary.`,
|
|
76
|
+
`Native stderr output is recorded unknown until its exact token/hash frame appears at a later prompt boundary; model comprehension is not observed.`,
|
|
78
77
|
];
|
|
79
78
|
}
|
package/dist/harness-links.js
CHANGED
|
@@ -30,8 +30,10 @@ export function installOpenCodePlugin(homeDir, force = false, run = runOpenCode)
|
|
|
30
30
|
// current-field entry; updating an exact old pin otherwise succeeds unchanged.
|
|
31
31
|
void force;
|
|
32
32
|
const version = openCodeCommand(homeDir, ['--version'], run, 'version check').trim();
|
|
33
|
-
const match = /^(\d+)\.\d+\.\d+(
|
|
34
|
-
|
|
33
|
+
const match = /^(?:opencode[ \t]+)?v?(\d+)\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
|
|
34
|
+
if (!match)
|
|
35
|
+
throw new Error(`Could not determine OpenCode version from ${JSON.stringify(version)}; check opencode --version output.`);
|
|
36
|
+
const major = Number(match[1]);
|
|
35
37
|
if (major !== 1 && major !== 2)
|
|
36
38
|
throw new Error(`Unsupported OpenCode version ${JSON.stringify(version)}; install OpenCode 1.x or 2.x and retry.`);
|
|
37
39
|
const configs = readOpenCodeConfigs(homeDir);
|
package/dist/help.js
CHANGED
|
@@ -60,7 +60,8 @@ const COMMANDS = [
|
|
|
60
60
|
{ names: ['codex-hook'], usage: '{command}', summary: 'Present pending attention at one native agent boundary.', hiddenFromIndex: true },
|
|
61
61
|
{ names: ['codex-watch'], usage: '{command}', summary: 'Observe pending attention from a native async Stop hook.', hiddenFromIndex: true },
|
|
62
62
|
{ names: ['codex-interrupt'], usage: '{command}', summary: 'Cancel pending attention at a native Interrupt boundary.', hiddenFromIndex: true },
|
|
63
|
-
{ names: ['claude-
|
|
63
|
+
{ names: ['claude-hook'], usage: '{command}', summary: 'Receive a native Claude hook event.', hiddenFromIndex: true },
|
|
64
|
+
{ names: ['claude-watch'], usage: '{command}', summary: 'Observe pending attention from a native async Stop hook.', hiddenFromIndex: true },
|
|
64
65
|
{
|
|
65
66
|
names: ['history'], usage: 'history [filters] [output]', usesSquare: true, group: 'participant',
|
|
66
67
|
summary: 'Read or search the archive without changing what you have caught.',
|
package/dist/mcp.js
CHANGED
|
@@ -24,7 +24,7 @@ export function createSquareMcpServer(options) {
|
|
|
24
24
|
const call = invoke(tool, args, { signal: extra.signal }, async () => {
|
|
25
25
|
const threadId = extra._meta?.threadId;
|
|
26
26
|
const opencodeSessionId = extra._meta?.['ai.opencode/sessionID'];
|
|
27
|
-
// Claude's plugin
|
|
27
|
+
// Claude's bundled plugin requires a per-call native hook association; startup identity is not a fallback.
|
|
28
28
|
const claude = baseEnv.SQUARE_CLAUDE_MCP_CONTEXT === '1'
|
|
29
29
|
? await readClaudeMcpCall(extra._meta?.['claudecode/toolUseId'], baseEnv)
|
|
30
30
|
: undefined;
|
|
@@ -46,7 +46,6 @@ export function createSquareMcpServer(options) {
|
|
|
46
46
|
: baseEnv;
|
|
47
47
|
const callWake = wakeAddress !== undefined && (owner === configuredOwner
|
|
48
48
|
|| wakeAddress.kind === 'paseo' && wakeAddress.address.agentId === baseEnv.PASEO_AGENT_ID
|
|
49
|
-
|| wakeAddress.kind === 'claude-native' && wakeAddress.address.sessionId === claude?.sessionId
|
|
50
49
|
|| wakeAddress.kind === 'opencode-server' && wakeAddress.address.sessionId === opencodeSessionId)
|
|
51
50
|
? wakeAddress : undefined;
|
|
52
51
|
return {
|
package/dist/notifications.js
CHANGED
|
@@ -31,9 +31,9 @@ export async function dispatchPostedCard(ledger, owner, key, track, env = proces
|
|
|
31
31
|
const configured = Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000);
|
|
32
32
|
const timeoutMs = Number.isSafeInteger(configured) && configured > 0 ? configured : 5000;
|
|
33
33
|
for (const route of session.routes) {
|
|
34
|
-
// Codex custody belongs to
|
|
35
|
-
// supply transports explicitly to the
|
|
36
|
-
if (
|
|
34
|
+
// Claude and Codex custody belongs to their native receivers, including for
|
|
35
|
+
// callers that supply transports explicitly to the producer API.
|
|
36
|
+
if (['codex-queue', 'claude-async-hook', 'claude-native'].includes(route.kind))
|
|
37
37
|
continue;
|
|
38
38
|
if (signal?.aborted)
|
|
39
39
|
return;
|
|
@@ -21,7 +21,7 @@ export default async function squareOpenCodeV1(ctx) {
|
|
|
21
21
|
throw new Error('native version below resident floor or unknown');
|
|
22
22
|
if (typeof ctx.client?.session?.get !== 'function' || typeof ctx.client.session.promptAsync !== 'function')
|
|
23
23
|
throw new Error('native session API unavailable');
|
|
24
|
-
const { DatabaseSync } = await import('
|
|
24
|
+
const { DatabaseSync } = await import('../../delivery-ledger-sqlite.js');
|
|
25
25
|
const db = new DatabaseSync(':memory:');
|
|
26
26
|
try {
|
|
27
27
|
db.exec('CREATE TABLE capability (value TEXT)');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { DeliveryTransport } from './delivery-ledger.js';
|
|
2
2
|
export type WakeTransports = readonly DeliveryTransport[];
|
|
3
|
-
/**
|
|
3
|
+
/** Claude now receives only at native hooks; producers never target a socket. */
|
|
4
4
|
export declare function createDefaultDeliveryTransports(env?: NodeJS.ProcessEnv): Promise<WakeTransports>;
|
package/dist/wake-transports.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** Claude now receives only at native hooks; producers never target a socket. */
|
|
2
2
|
export async function createDefaultDeliveryTransports(env = process.env) {
|
|
3
|
-
const { createClaudeWakeTransport } = await import('./claude-transport.js');
|
|
4
3
|
const { HttpWakeTransport } = await import('./http-delivery.js');
|
|
5
|
-
const transports = [
|
|
4
|
+
const transports = [new HttpWakeTransport({ env })];
|
|
6
5
|
if (env.SQUARE_DISABLE_PASEO_WAKE !== '1') {
|
|
7
6
|
const { PaseoWakeTransport } = await import('./paseo-delivery.js');
|
|
8
7
|
transports.push(new PaseoWakeTransport());
|
package/guides/claude-native.md
CHANGED
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
# Claude
|
|
1
|
+
# Claude Code delivery
|
|
2
2
|
|
|
3
|
-
Square
|
|
4
|
-
session association; a missing or expired association fails closed. A session ID or socket alone does
|
|
5
|
-
not establish that the mod is loaded. The supported native path requires macOS Claude Code with the
|
|
6
|
-
mod loaded and approved. Square does not alter inbound policy or bypass a held peer message.
|
|
3
|
+
The Square Claude plugin uses ordinary native command hooks. It has no mod, native inbox, or producer socket route. `SessionStart` attaches the exact native session to the host delivery ledger; `SessionEnd` fences and detaches that captured attachment without changing Square membership.
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
Square; host end does not land `done`, and start/resume does not restore seats. See
|
|
10
|
-
[participant-ownership.md](participant-ownership.md).
|
|
5
|
+
At `UserPromptSubmit`, Square records Claude's native `prompt_id` before presenting pending cards through event-correct `additionalContext`. Tool boundaries (`PreToolUse`, `PostToolUse`, `PostToolUseFailure`) use the same boundary presenter for the exact native main/child owner, so cards arriving during a running turn need not wait for another user prompt. Autonomous hook-generated prompts are boundaries too, not evidence of human approval. A later prompt records presentation only when its input contains the exact complete prepared frame, including the ledger token and stored full-frame hash. That establishes observation before the model processes the prompt, not comprehension. The async frame uses a plain warning and token delimiters; Square never decodes entities or accepts changed/truncated text as an exact observation.
|
|
11
6
|
|
|
12
|
-
|
|
13
|
-
handoff as accepted, not presented; the stored main user-role text observation settles presentation
|
|
14
|
-
through its handoff reference. This establishes observation at that boundary, not comprehension, and
|
|
15
|
-
cannot promise that a later plugin leaves the card intact. Unknown native custody is not a safe reason
|
|
16
|
-
to replay through another route. A shortened activity card points to catch; Square only folds a receipt
|
|
17
|
-
for a full card into its read state. Direct producer notices retain their full body.
|
|
7
|
+
At a normal `Stop`, one per-owner OS-locked observer waits for ledger changes for at most 86,400 seconds. It rechecks the exact captured `prompt_id`, attachment generation, Stop epoch and route after acquiring the lock and immediately before output. A new prompt disarms old observers. The watcher claims one card at a time and uses the existing handoff token/hash correlation. Invoked stderr output is `unknown` until that exact frame is observed at a later prompt boundary; there is no automatic fallback replay. Missing prompt IDs fail closed.
|
|
18
8
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
This is a finite, boundary-driven observer, not a promise of continuous coverage. Missing Stop, timeout, process exit, headless teardown, or a busy Escape with no later normal Stop may leave cards pending. Idle Escape has no classic hook and may not disarm an already waiting observer; already emitted asyncRewake notifications cannot be recalled. No Square inbox fallback is retained for these cases.
|
|
10
|
+
|
|
11
|
+
The bundled MCP server gets call-local session, cwd, and agent attribution from native `PreToolUse`/terminal tool hooks keyed by Claude's `claudecode/toolUseId` metadata. Duplicate or incomplete call identity is denied by the event-specific PreToolUse permission response; unrelated tool hooks do not block. Cleanup matches tool-use ID, session and agent. Interrupted calls without a terminal hook expire after ten minutes. Models cannot supply identity as tool arguments.
|
|
12
|
+
|
|
13
|
+
The installed and tested native runtime evidence is macOS Claude Code 2.1.299 with isolated HOME, plugin settings, delivery ledger and a dummy-key loopback provider. This does not certify other platforms, versions, or headless behavior. The final native prompt/generation check and async hook completion are not atomic. `node --import ./test/sandbox-env.js test/claude-hook-native.mjs` with `SQUARE_CLAUDE_HOOK_NATIVE_SMOKE=1` runs the shipped manifest and candidate CLI through a scratch PATH shim and preserves hook/provider evidence in `/tmp`. It proves repeated idle wake, prompt/attachment fencing, duplicate suppression, active prompt/tool context, native main/child MCP attribution and identity-failure denial.
|
package/guides/mcp.md
CHANGED
|
@@ -6,7 +6,7 @@ Set `SQUARE_LOCATION` to the default `.square` artifact path; calls may override
|
|
|
6
6
|
|
|
7
7
|
The server exposes `join`, `express`, `catch`, `history`, `attention`, `done`, and `status`. `attention` takes `action: listen | unlisten | ignore`; `status` also reports who you listen to when a name resolves. `catch` consumes activities. `history` is read-only and accepts stable `act/<index>` cursors. All seven tools return the same human-readable text as the CLI in `content`, without `outputSchema` or `structuredContent`. Square failures set `isError: true` and return readable refusal and recovery text in `content`. Clients keep their native MCP result envelope; machine-readable domain results remain available through the SDK.
|
|
8
8
|
|
|
9
|
-
Claude's bundled plugin configures this server automatically.
|
|
9
|
+
Claude's bundled plugin configures this server automatically. Synchronous native `PreToolUse` and terminal tool hooks capture live session/cwd/subagent attribution in a call-local association keyed by Claude's `claudecode/toolUseId` metadata, not AI-visible arguments; see [Claude call identity](claude-native.md). Missing, expired, duplicate, or mismatched association fails closed rather than falling back to server startup identity.
|
|
10
10
|
|
|
11
11
|
Tool definitions and argument-to-`Square`/`Participant` calls live in `square-tools.ts`, shared with the [Pi native tools](pi-tools.md). Domain receipts stay internal to these adapters; tools return only their rendered text. All callers use the [same concrete resource API](square-api.md), with no alternate factory or generic outcome envelope. `express.noMention` disables body @name scanning without disabling explicit mentions or reply attention.
|
|
12
12
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "square",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Native Claude Code
|
|
5
|
-
"types": "./types.d.ts",
|
|
3
|
+
"version": "0.4.3",
|
|
4
|
+
"description": "Native Claude Code hook delivery for Square participants",
|
|
6
5
|
"author": {
|
|
7
6
|
"name": "Square"
|
|
8
7
|
}
|
|
@@ -1 +1,11 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"SessionStart": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
|
|
4
|
+
"SessionEnd": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 3 }] }],
|
|
5
|
+
"UserPromptSubmit": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
|
|
6
|
+
"PreToolUse": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
|
|
7
|
+
"PostToolUse": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
|
|
8
|
+
"PostToolUseFailure": [{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }],
|
|
9
|
+
"Stop": [{ "hooks": [{ "type": "command", "command": "square claude-watch", "asyncRewake": true, "timeout": 86400 }] }]
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -23,7 +23,7 @@ export default async function squareOpenCodeV1(ctx: LegacyContext) {
|
|
|
23
23
|
const match = typeof health.version === 'string' && /^(\d+)\.(\d+)\.(\d+)$/.exec(health.version);
|
|
24
24
|
if (!match || Number(match[1]) !== 1 || Number(match[2]) < 15 || (Number(match[2]) === 15 && Number(match[3]) < 11)) throw new Error('native version below resident floor or unknown');
|
|
25
25
|
if (typeof ctx.client?.session?.get !== 'function' || typeof ctx.client.session.promptAsync !== 'function') throw new Error('native session API unavailable');
|
|
26
|
-
const { DatabaseSync } = await import('
|
|
26
|
+
const { DatabaseSync } = await import('../../delivery-ledger-sqlite.js');
|
|
27
27
|
const db = new DatabaseSync(':memory:');
|
|
28
28
|
try { db.exec('CREATE TABLE capability (value TEXT)'); db.prepare('INSERT INTO capability VALUES (?)').run('ok'); db.prepare('SELECT value FROM capability').get(); }
|
|
29
29
|
finally { db.close(); }
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { type DeliveryHandoffRef } from './delivery-ledger.js';
|
|
2
|
-
export { createClaudeWakeTransport, nativeTokens, nativePayload } from './claude-transport.js';
|
|
3
|
-
export type ClaudeBinding = {
|
|
4
|
-
readonly session: string;
|
|
5
|
-
readonly generation: string;
|
|
6
|
-
};
|
|
7
|
-
/** Extract complete frames and hash the exact bytes observed by the native host. */
|
|
8
|
-
export declare function claudeFrames(text: string): readonly {
|
|
9
|
-
token: string;
|
|
10
|
-
payloadHash: string;
|
|
11
|
-
}[];
|
|
12
|
-
/** Guard pre-effect; admit and observe only the exact captured handoff after each native effect. */
|
|
13
|
-
export declare function observeClaudeDelivery(sessionId: string, text: string, operation: 'guard' | 'admitted' | 'stored', env?: NodeJS.ProcessEnv, captured?: readonly DeliveryHandoffRef[]): Promise<{
|
|
14
|
-
recognized: boolean;
|
|
15
|
-
current: boolean;
|
|
16
|
-
refs?: readonly DeliveryHandoffRef[];
|
|
17
|
-
}>;
|
package/dist/claude-delivery.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { DeliveryLedger } from './delivery-ledger.js';
|
|
2
|
-
import { harnessOwner } from './owner.js';
|
|
3
|
-
import { renderDeliveryReminder } from './delivery-frame.js';
|
|
4
|
-
import { hashPayload, nativeTokens } from './claude-transport.js';
|
|
5
|
-
export { createClaudeWakeTransport, nativeTokens, nativePayload } from './claude-transport.js';
|
|
6
|
-
/** Extract complete frames and hash the exact bytes observed by the native host. */
|
|
7
|
-
export function claudeFrames(text) {
|
|
8
|
-
const prefix = renderDeliveryReminder([''], { tagged: false });
|
|
9
|
-
return [...text.matchAll(/(?:^|\n)(\[square-inbox:([a-zA-Z0-9-]+)\]\n[\s\S]*?\n\[\/square-inbox:\2\])(?=\n|$)/g)]
|
|
10
|
-
.flatMap(frame => {
|
|
11
|
-
const start = frame.index + frame[0].length - frame[1].length;
|
|
12
|
-
return text.slice(start - prefix.length, start) === prefix
|
|
13
|
-
? [{ token: frame[2], payloadHash: hashPayload(prefix + frame[1]) }] : [];
|
|
14
|
-
});
|
|
15
|
-
}
|
|
16
|
-
/** Guard pre-effect; admit and observe only the exact captured handoff after each native effect. */
|
|
17
|
-
export async function observeClaudeDelivery(sessionId, text, operation, env = process.env, captured) {
|
|
18
|
-
const tokens = nativeTokens(text);
|
|
19
|
-
if (!tokens.length)
|
|
20
|
-
return { recognized: false, current: false };
|
|
21
|
-
const frames = claudeFrames(text);
|
|
22
|
-
const owner = harnessOwner({ channel: 'claude-code', session: sessionId });
|
|
23
|
-
const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY });
|
|
24
|
-
try {
|
|
25
|
-
const found = await Promise.all(tokens.map(token => ledger.handoff(token)));
|
|
26
|
-
const recognized = found.some(Boolean);
|
|
27
|
-
if (!recognized)
|
|
28
|
-
return { recognized: false, current: false };
|
|
29
|
-
if (frames.length !== tokens.length || new Set(tokens).size !== tokens.length || found.some(ref => !ref))
|
|
30
|
-
return { recognized, current: false };
|
|
31
|
-
const refs = found;
|
|
32
|
-
if (refs.some((ref, i) => ref.owner !== owner || ref.routeKind !== 'claude-native' || ref.token !== frames[i].token || ref.payloadHash !== frames[i].payloadHash))
|
|
33
|
-
return { recognized, current: false };
|
|
34
|
-
if (operation === 'admitted' && (!Array.isArray(captured) || captured.length !== refs.length || refs.some((ref, i) => {
|
|
35
|
-
const old = captured[i];
|
|
36
|
-
return !old || old.token !== ref.token || old.owner !== ref.owner || old.generation !== ref.generation || old.payloadHash !== ref.payloadHash || old.routeKind !== ref.routeKind;
|
|
37
|
-
})))
|
|
38
|
-
return { recognized, current: false };
|
|
39
|
-
if (operation === 'guard') {
|
|
40
|
-
for (const ref of refs)
|
|
41
|
-
if (!await ledger.guardHandoff(ref))
|
|
42
|
-
return { recognized, current: false };
|
|
43
|
-
return { recognized, current: true, refs };
|
|
44
|
-
}
|
|
45
|
-
for (const ref of refs) {
|
|
46
|
-
const recorded = operation === 'admitted' ? await ledger.acceptHandoff(ref) : await ledger.observePresentation(ref);
|
|
47
|
-
if (!recorded)
|
|
48
|
-
return { recognized, current: false };
|
|
49
|
-
}
|
|
50
|
-
return { recognized, current: true };
|
|
51
|
-
}
|
|
52
|
-
finally {
|
|
53
|
-
await ledger.close();
|
|
54
|
-
}
|
|
55
|
-
}
|
package/dist/claude-mod.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export interface ClaudeCoordinate {
|
|
2
|
-
readonly sessionId: string;
|
|
3
|
-
readonly cwd: string;
|
|
4
|
-
readonly version: string;
|
|
5
|
-
readonly endpoint?: string;
|
|
6
|
-
}
|
|
7
|
-
/** JSON stdin bridge. Identity is explicit per operation; process.env is never rewritten. */
|
|
8
|
-
export declare function runClaudeMod(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
package/dist/claude-mod.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { observeClaudeDelivery } from './claude-delivery.js';
|
|
2
|
-
import { beginClaudeMcpCall, endClaudeMcpCall } from './claude-mcp-context.js';
|
|
3
|
-
import { DeliveryLedger } from './delivery-ledger.js';
|
|
4
|
-
import { attachDeliverySessionRoutes, detachDeliverySession } from './session-lifecycle.js';
|
|
5
|
-
import { harnessOwner } from './owner.js';
|
|
6
|
-
/** JSON stdin bridge. Identity is explicit per operation; process.env is never rewritten. */
|
|
7
|
-
export async function runClaudeMod(inputText, env = process.env) {
|
|
8
|
-
if (inputText.length > 512_000)
|
|
9
|
-
throw new Error('Claude bridge input exceeds its bound.');
|
|
10
|
-
const input = JSON.parse(inputText);
|
|
11
|
-
if (!input || typeof input.sessionId !== 'string' || !input.sessionId || typeof input.cwd !== 'string')
|
|
12
|
-
throw new Error('Claude bridge coordinate missing.');
|
|
13
|
-
if (input.operation === 'tool-begin') {
|
|
14
|
-
return `${JSON.stringify(await beginClaudeMcpCall({ toolUseId: input.toolUseId, sessionId: input.sessionId, cwd: input.cwd, agentId: input.agentId }, env))}\n`;
|
|
15
|
-
}
|
|
16
|
-
if (input.operation === 'tool-end') {
|
|
17
|
-
await endClaudeMcpCall(input.toolUseId, input.token, env);
|
|
18
|
-
return '{}\n';
|
|
19
|
-
}
|
|
20
|
-
const owner = harnessOwner({ channel: 'claude-code', session: input.sessionId, ...(input.agentId === undefined ? {} : { agent: input.agentId }) });
|
|
21
|
-
const supported = process.platform === 'darwin' && typeof input.endpoint === 'string' && input.endpoint.startsWith('/');
|
|
22
|
-
const routes = supported ? [{ kind: 'claude-native', address: { sessionId: input.sessionId, endpoint: input.endpoint, version: input.version, platform: 'darwin', loaded: 'true' } }] : [];
|
|
23
|
-
const captured = input.bindings?.find(binding => binding.session === input.sessionId)?.generation;
|
|
24
|
-
if (input.operation === 'start') {
|
|
25
|
-
const session = await attachDeliverySessionRoutes(owner, env, routes, AbortSignal.timeout(2500));
|
|
26
|
-
if (session && !supported && session.routes.some(route => route.kind === 'claude-native')) {
|
|
27
|
-
const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY });
|
|
28
|
-
try {
|
|
29
|
-
await ledger.updateRoutes({ owner, generation: session.generation, routes: session.routes.filter(route => route.kind !== 'claude-native') });
|
|
30
|
-
}
|
|
31
|
-
finally {
|
|
32
|
-
await ledger.close();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return `${JSON.stringify({ available: supported, diagnostic: supported ? undefined : 'Native inbox unavailable: requires macOS Claude with an interactive loaded mod and its real endpoint.', bindings: session ? [{ session: input.sessionId, generation: session.generation }] : [] })}\n`;
|
|
36
|
-
}
|
|
37
|
-
if (input.operation === 'end') {
|
|
38
|
-
const retired = captured && await detachDeliverySession(owner, captured, env);
|
|
39
|
-
return `${JSON.stringify({ retired: retired ? input.bindings : [] })}\n`;
|
|
40
|
-
}
|
|
41
|
-
if (input.operation === 'reconcile' || input.operation === 'cancel') {
|
|
42
|
-
const ledger = await DeliveryLedger.open({ location: env.SQUARE_DELIVERY });
|
|
43
|
-
try {
|
|
44
|
-
const session = await ledger.session(owner);
|
|
45
|
-
if (captured && session?.generation === captured && session.state === 'attached') {
|
|
46
|
-
if (input.operation === 'cancel')
|
|
47
|
-
await ledger.cancel({ owner, generation: captured });
|
|
48
|
-
else
|
|
49
|
-
await ledger.updateRoutes({ owner, generation: captured, routes: [...session.routes.filter(route => route.kind !== 'claude-native'), ...routes] });
|
|
50
|
-
}
|
|
51
|
-
return `${JSON.stringify({ available: supported, diagnostic: supported ? undefined : 'Native inbox unavailable: requires macOS Claude with an interactive loaded mod and its real endpoint.', bindings: input.bindings ?? [], cancelledBindings: input.bindings ?? [] })}\n`;
|
|
52
|
-
}
|
|
53
|
-
finally {
|
|
54
|
-
await ledger.close();
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (!['guard', 'admitted', 'stored'].includes(input.operation) || typeof input.text !== 'string')
|
|
58
|
-
throw new Error('Unknown Claude bridge operation.');
|
|
59
|
-
return `${JSON.stringify(await observeClaudeDelivery(input.sessionId, input.text, input.operation, env, input.refs))}\n`;
|
|
60
|
-
}
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { DeliveryTransport } from './delivery-ledger.js';
|
|
2
|
-
export declare function nativeTokens(text: string): string[];
|
|
3
|
-
export declare function nativePayload(token: string, text: string): string;
|
|
4
|
-
export declare function hashPayload(payload: string): string;
|
|
5
|
-
export declare function createClaudeWakeTransport(env?: NodeJS.ProcessEnv): DeliveryTransport;
|
package/dist/claude-transport.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { stat } from 'node:fs/promises';
|
|
2
|
-
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { renderDeliveryReminder } from './delivery-frame.js';
|
|
4
|
-
import { writeClaudeNative } from './packages/agent-delivery/src/claude-native.js';
|
|
5
|
-
const marker = /(?:^|\n)\[square-inbox:([a-zA-Z0-9-]+)\](?:\n|$)/g;
|
|
6
|
-
export function nativeTokens(text) { return [...text.matchAll(marker)].map(match => match[1]); }
|
|
7
|
-
export function nativePayload(token, text) {
|
|
8
|
-
return renderDeliveryReminder([`[square-inbox:${token}]\n${text}\n[/square-inbox:${token}]`], { tagged: false });
|
|
9
|
-
}
|
|
10
|
-
export function hashPayload(payload) { return createHash('sha256').update(payload).digest('hex'); }
|
|
11
|
-
export function createClaudeWakeTransport(env = process.env) {
|
|
12
|
-
return {
|
|
13
|
-
kind: 'claude-native',
|
|
14
|
-
async send(card, address, context) {
|
|
15
|
-
if (address.loaded !== 'true' || !address.endpoint || !address.sessionId)
|
|
16
|
-
return 'not-capable';
|
|
17
|
-
try {
|
|
18
|
-
if (!(await stat(address.endpoint)).isSocket())
|
|
19
|
-
return 'not-capable';
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return 'not-capable';
|
|
23
|
-
}
|
|
24
|
-
const messageId = randomUUID();
|
|
25
|
-
const payload = nativePayload(context.token, card.text);
|
|
26
|
-
if (!await context.prepare({ token: context.token, messageId, payloadHash: hashPayload(payload) }))
|
|
27
|
-
return 'not-capable';
|
|
28
|
-
if (context.signal.aborted || Date.now() >= context.deadline)
|
|
29
|
-
return 'not-capable';
|
|
30
|
-
const result = await writeClaudeNative({ sessionId: address.sessionId, endpoint: address.endpoint, messageId, env }, payload, { deadline: context.deadline, signal: context.signal, beforeSend: context.beforeSend });
|
|
31
|
-
return result.outcome === 'unavailable' ? 'failed' : 'unknown';
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
// Claude Code mod. No Node imports, shell interpolation, or child credentials.
|
|
2
|
-
const hasToken = (text) => typeof text === 'string' && /(?:^|\n)\[square-inbox:[a-zA-Z0-9-]+\](?:\n|$)/.test(text);
|
|
3
|
-
|
|
4
|
-
async function coordinate($) {
|
|
5
|
-
return { sessionId: await $.session.id(), cwd: await $.session.cwd(), version: (await $.session.version()).version, endpoint: await $.env.get('CLAUDE_CODE_MESSAGING_SOCKET') };
|
|
6
|
-
}
|
|
7
|
-
async function bridge($, value, budget = 2800) {
|
|
8
|
-
const result = await $.process.run(['square', 'claude-mod'], { cwd: value.cwd, stdin: JSON.stringify(value), timeoutMs: Math.max(1, Math.min(2800, budget)) });
|
|
9
|
-
if (result.exitCode !== 0 || result.isStdoutTruncated) throw new Error('Square native inbox bridge unavailable');
|
|
10
|
-
return JSON.parse(result.stdout);
|
|
11
|
-
}
|
|
12
|
-
async function snapshot($, id) { return (await $.state.get({ plugin: 'square', key: 'inbox', id })).value; }
|
|
13
|
-
async function cancellation($, id) { return (await $.state.get({ plugin: 'square', key: 'cancelAt', id })).value; }
|
|
14
|
-
function confirmLater($, c, operation, text, refs) {
|
|
15
|
-
// State reads are dispatch snapshots. This ONE fresh timer dispatch is not polling.
|
|
16
|
-
// Reload cancels it; lost evidence stays unknown and is never blindly replayed.
|
|
17
|
-
try { $.clock.after(0, () => {
|
|
18
|
-
(async () => {
|
|
19
|
-
await bridge($, { ...c, operation, text, refs });
|
|
20
|
-
})().catch(() => undefined);
|
|
21
|
-
}); } catch { /* denied scheduling leaves evidence unknown, never a second next */ }
|
|
22
|
-
}
|
|
23
|
-
async function save($, id, update, version) {
|
|
24
|
-
return $.state.set({ plugin: 'square', key: 'inbox', id }, update, version === undefined ? undefined : { ifVersion: version });
|
|
25
|
-
}
|
|
26
|
-
async function reconcile($, sessionId, operation = 'start', resume = false) {
|
|
27
|
-
const read = await coordinate($);
|
|
28
|
-
const c = sessionId ? { ...read, sessionId } : read;
|
|
29
|
-
const held = await $.state.get({ plugin: 'square', key: 'inbox', id: c.sessionId });
|
|
30
|
-
const old = held.value;
|
|
31
|
-
const cancelled = await cancellation($, c.sessionId);
|
|
32
|
-
const result = await bridge($, { ...c, operation, cancelAt: cancelled?.at, cancelledBindings: cancelled?.bindings, bindings: old?.bindings, resume });
|
|
33
|
-
// A concurrent abort must win over a stale reconciliation write.
|
|
34
|
-
await save($, c.sessionId, { coordinate: c, bindings: result.bindings }, held.version);
|
|
35
|
-
if (!result.available) await $.ui.log(result.diagnostic);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function cancel($) {
|
|
39
|
-
const c = await coordinate($);
|
|
40
|
-
const old = await snapshot($, c.sessionId);
|
|
41
|
-
const cancelAt = await $.clock.now();
|
|
42
|
-
const captured = old?.bindings ?? [];
|
|
43
|
-
await $.state.set({ plugin: 'square', key: 'cancelAt', id: c.sessionId }, { at: cancelAt, bindings: captured });
|
|
44
|
-
const result = await bridge($, { ...c, bindings: captured, cancelAt, cancelledBindings: captured, operation: 'cancel' });
|
|
45
|
-
try { $.clock.after(0, () => {
|
|
46
|
-
(async () => {
|
|
47
|
-
const held = await $.state.get({ plugin: 'square', key: 'cancelAt', id: c.sessionId });
|
|
48
|
-
if (held.value?.at === cancelAt) await $.state.set({ plugin: 'square', key: 'cancelAt', id: c.sessionId }, { at: cancelAt, bindings: result.cancelledBindings }, { ifVersion: held.version });
|
|
49
|
-
})().catch(() => undefined);
|
|
50
|
-
}); } catch { /* captured cancellation remains engine-owned */ }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function register(on) {
|
|
54
|
-
on('session.start', async ($, e, next) => {
|
|
55
|
-
await reconcile($).catch(() => undefined);
|
|
56
|
-
return next(e);
|
|
57
|
-
});
|
|
58
|
-
on('classic.SessionStart', async ($, e, next) => {
|
|
59
|
-
await reconcile($, e.session_id, 'start', e.source === 'resume').catch(() => undefined);
|
|
60
|
-
return next(e);
|
|
61
|
-
});
|
|
62
|
-
on('tool.call', async ($, e, next) => {
|
|
63
|
-
try {
|
|
64
|
-
if (typeof e.tool !== 'string' || !e.tool.startsWith('mcp__plugin_square_square__')) return await next(e);
|
|
65
|
-
next.signal.throwIfAborted();
|
|
66
|
-
const c = await coordinate($);
|
|
67
|
-
const call = { ...c, toolUseId: e.tool_use_id, agentId: e.agentId };
|
|
68
|
-
const { token } = await bridge($, { ...call, operation: 'tool-begin' });
|
|
69
|
-
try {
|
|
70
|
-
next.signal.throwIfAborted();
|
|
71
|
-
// MCP receives the original business arguments; identity travels only by tool-use ID.
|
|
72
|
-
return await next(e);
|
|
73
|
-
} finally {
|
|
74
|
-
await bridge($, { ...call, token, operation: 'tool-end' }).catch(() => undefined);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
finally { if (!e.agentId) await reconcile($, undefined, 'reconcile').catch(() => undefined); }
|
|
78
|
-
});
|
|
79
|
-
on('turn.start', async ($, e, next) => {
|
|
80
|
-
if (!e.agentId) await reconcile($, undefined, 'reconcile').catch(() => undefined);
|
|
81
|
-
return next(e);
|
|
82
|
-
});
|
|
83
|
-
on('session.end', async ($, e, next) => {
|
|
84
|
-
// Pinned old ID and captured seat generations, never a later current coordinate.
|
|
85
|
-
const endedAt = await $.clock.now().catch(() => undefined);
|
|
86
|
-
const held = await $.state.get({ plugin: 'square', key: 'inbox', id: e.sessionId }).catch(() => undefined);
|
|
87
|
-
const old = held?.value;
|
|
88
|
-
const c = old?.coordinate ?? await coordinate($).catch(() => undefined);
|
|
89
|
-
if (c) {
|
|
90
|
-
const result = await bridge($, { ...c, sessionId: e.sessionId, bindings: old?.bindings, endedAt, operation: 'end' }, next.budget.remainingMs).catch(() => undefined);
|
|
91
|
-
// Retired membership lives in the host ledger; conversation state may be cleared.
|
|
92
|
-
if (result) await save($, e.sessionId, { coordinate: { ...c, sessionId: e.sessionId }, bindings: [] }, held?.version).catch(() => undefined);
|
|
93
|
-
}
|
|
94
|
-
return next(e);
|
|
95
|
-
});
|
|
96
|
-
on('turn.abort', async ($, e, next) => {
|
|
97
|
-
await cancel($).catch(() => undefined);
|
|
98
|
-
return next(e);
|
|
99
|
-
});
|
|
100
|
-
on('turn.complete', async ($, e, next) => {
|
|
101
|
-
// Terminal Esc aborts are reported here, not through the plugin's turn.abort op.
|
|
102
|
-
if (!e.agentId && e.reason === 'aborted') await cancel($).catch(() => undefined);
|
|
103
|
-
return next(e);
|
|
104
|
-
});
|
|
105
|
-
on('session.receive', async ($, e, next) => {
|
|
106
|
-
if (e.agentId || e.origin?.kind !== 'peer' || !hasToken(e.text)) return next(e);
|
|
107
|
-
const c = await coordinate($).catch(() => undefined);
|
|
108
|
-
if (!c) return next(e);
|
|
109
|
-
const old = await snapshot($, c.sessionId).catch(() => undefined);
|
|
110
|
-
const cancelled = await cancellation($, c.sessionId).catch(() => undefined);
|
|
111
|
-
const guard = await bridge($, { ...c, cancelAt: cancelled?.at, cancelledBindings: cancelled?.bindings, bindings: old?.bindings, operation: 'guard', text: e.text }).catch(() => undefined);
|
|
112
|
-
if (!guard || (guard.recognized && !guard.current)) return { consumed: 'Square delivery is stale, cancelled, or unavailable' };
|
|
113
|
-
// Preserve native policy: hold/refuse reject this gate; never call next twice.
|
|
114
|
-
const result = await next(e);
|
|
115
|
-
if (guard?.current && result.text === e.text) confirmLater($, c, 'admitted', result.text, guard.refs);
|
|
116
|
-
return result;
|
|
117
|
-
});
|
|
118
|
-
on('session.append', async ($, e, next) => {
|
|
119
|
-
if (e.agentId || e.origin?.kind !== 'peer') return next(e);
|
|
120
|
-
const c = await coordinate($).catch(() => undefined);
|
|
121
|
-
const result = await next(e);
|
|
122
|
-
if (!c || result.message?.role !== 'user') return result;
|
|
123
|
-
// Match a complete payload inside one returned stored text block, not entry text.
|
|
124
|
-
for (const block of result.message.content ?? []) {
|
|
125
|
-
if (block.type === 'text' && hasToken(block.text)) confirmLater($, c, 'stored', block.text);
|
|
126
|
-
}
|
|
127
|
-
return result;
|
|
128
|
-
});
|
|
129
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export type SquareInboxBinding = { location: string; participant: string; session: string; generation?: string };
|
|
2
|
-
export type SquareInboxCoordinate = { sessionId: string; cwd: string; version: string; endpoint?: string };
|
|
3
|
-
export type SquareInboxCancellation = { at: number; bindings?: SquareInboxBinding[] };
|
|
4
|
-
export type SquareInboxState = { coordinate: SquareInboxCoordinate; bindings?: SquareInboxBinding[] };
|
|
5
|
-
declare module 'claude-code' {
|
|
6
|
-
interface PluginState { square: { inbox: StateFamily<SquareInboxState>; cancelAt: StateFamily<SquareInboxCancellation> } }
|
|
7
|
-
}
|