@astrosheep/square 0.4.7 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/card-format.d.ts +2 -0
  2. package/dist/card-format.js +2 -0
  3. package/dist/claude-mcp-context.d.ts +0 -1
  4. package/dist/claude-mcp-context.js +0 -10
  5. package/dist/claude-mod.d.ts +2 -0
  6. package/dist/claude-mod.js +211 -0
  7. package/dist/cli/context.d.ts +1 -1
  8. package/dist/cli/context.js +1 -12
  9. package/dist/cli/express-presentation.js +6 -33
  10. package/dist/cli/json.js +1 -1
  11. package/dist/cli/observation-commands.d.ts +2 -3
  12. package/dist/cli/observation-commands.js +21 -59
  13. package/dist/cli/registry.js +3 -5
  14. package/dist/cli/square-commands.d.ts +1 -2
  15. package/dist/cli/square-commands.js +7 -17
  16. package/dist/decisions.js +5 -4
  17. package/dist/harness-claude.js +3 -3
  18. package/dist/help.js +6 -8
  19. package/dist/mcp.js +1 -1
  20. package/dist/neutral.d.ts +15 -1
  21. package/dist/notifications.js +1 -1
  22. package/dist/operation-presentation.d.ts +17 -15
  23. package/dist/operation-presentation.js +58 -32
  24. package/dist/packages/agent-delivery/src/index.d.ts +4 -9
  25. package/dist/packages/agent-delivery/src/index.js +0 -9
  26. package/dist/packages/agent-delivery/src/local-endpoint.d.ts +2 -12
  27. package/dist/packages/agent-delivery/src/local-endpoint.js +4 -48
  28. package/dist/packages/agent-delivery/src/pi-client.js +0 -2
  29. package/dist/packages/agent-delivery/src/types.d.ts +3 -37
  30. package/dist/plugins/paseo/shared/card-format.d.ts +6 -0
  31. package/dist/plugins/paseo/shared/card-format.js +38 -0
  32. package/dist/plugins/paseo/shared/card-presentation.d.ts +3 -1
  33. package/dist/plugins/paseo/shared/card-presentation.js +3 -1
  34. package/dist/presentation.d.ts +2 -20
  35. package/dist/presentation.js +11 -46
  36. package/dist/sqlite-square.js +5 -6
  37. package/dist/square-facade.d.ts +3 -12
  38. package/dist/square-posting.js +1 -1
  39. package/dist/square-tools.d.ts +0 -11
  40. package/dist/square-tools.js +33 -119
  41. package/dist/square-values.d.ts +1 -1
  42. package/dist/square-values.js +1 -1
  43. package/dist/time.d.ts +1 -0
  44. package/dist/time.js +10 -0
  45. package/dist/views.d.ts +1 -1
  46. package/dist/views.js +2 -2
  47. package/dist/wake-transports.d.ts +1 -1
  48. package/dist/wake-transports.js +1 -1
  49. package/guides/catch-cold-path-optimization.md +1 -1
  50. package/guides/claude-native.md +37 -7
  51. package/guides/delivery-primitives.md +2 -1
  52. package/guides/mcp.md +3 -3
  53. package/guides/pi-native-delivery.md +10 -2
  54. package/guides/pi-tools.md +1 -3
  55. package/guides/row-storage-design.md +1 -1
  56. package/package.json +1 -1
  57. package/plugins/claude/.claude-plugin/plugin.json +3 -2
  58. package/plugins/claude/README.md +9 -0
  59. package/plugins/claude/hooks/hooks.json +1 -9
  60. package/plugins/claude/hooks/register.js +178 -0
  61. package/plugins/claude/skills/square/SKILL.md +2 -2
  62. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  63. package/plugins/paseo/OVERVIEW.md +1 -1
  64. package/plugins/paseo/README.md +20 -23
  65. package/plugins/paseo/client/markdown.tsx +88 -95
  66. package/plugins/paseo/client/palette.ts +37 -12
  67. package/plugins/paseo/client/square-message.tsx +52 -76
  68. package/plugins/paseo/client/transform.ts +4 -6
  69. package/plugins/paseo/shared/card-format.ts +37 -0
  70. package/plugins/paseo/shared/card-presentation.ts +6 -2
  71. package/plugins/paseo/shared/message.ts +1 -6
  72. package/plugins/pi/square-pi.js +69 -50
  73. package/skills/square/SKILL.md +2 -2
  74. package/dist/claude-boundary-state.d.ts +0 -20
  75. package/dist/claude-boundary-state.js +0 -84
  76. package/dist/claude-hook.d.ts +0 -2
  77. package/dist/claude-hook.js +0 -232
  78. package/dist/packages/agent-delivery/src/claude-auth.d.ts +0 -13
  79. package/dist/packages/agent-delivery/src/claude-auth.js +0 -75
  80. package/dist/packages/agent-delivery/src/claude-native.d.ts +0 -30
  81. package/dist/packages/agent-delivery/src/claude-native.js +0 -75
  82. package/dist/packages/agent-delivery/src/claude.d.ts +0 -5
  83. package/dist/packages/agent-delivery/src/claude.js +0 -66
@@ -1,4 +1,5 @@
1
- import { Box, Markdown, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
1
+ import { Box, Markdown, stripTerminalSequences, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
2
+ import { cardReferences, reachLabel, timeLabel } from '../../dist/card-format.js';
2
3
  import { z } from 'zod/v4';
3
4
 
4
5
  import { Square, discover } from '../../dist/square-api.js';
@@ -25,81 +26,99 @@ function markdownTheme(theme) {
25
26
  heading: color('mdHeading'), link: color('mdLink'), linkUrl: color('mdLinkUrl'), code: color('mdCode'),
26
27
  codeBlock: color('mdCodeBlock'), codeBlockBorder: color('mdCodeBlockBorder'), quote: color('mdQuote'),
27
28
  quoteBorder: color('mdQuoteBorder'), hr: color('mdHr'), listBullet: color('mdListBullet'),
28
- bold: style(theme, 'bold'), italic: (text) => fg(theme, 'muted')(style(theme, 'italic')(text)),
29
+ // The inline parser has already applied the body foreground; let muted win without dropping bold/link styles.
30
+ bold: style(theme, 'bold'), italic: (text) => fg(theme, 'muted')(style(theme, 'italic')(
31
+ text.replace(/\x1b\[(?:38;[\d;]+|3[0-79]|9[0-7])m/g, ''))),
29
32
  underline: style(theme, 'underline'), strikethrough: style(theme, 'strikethrough'),
30
33
  };
31
34
  }
32
35
 
33
- /** Keep the short references; the long origin is the part that may ellipsize. */
36
+ /** Keep references intact before spending the remaining columns on the origin. */
34
37
  function cardHeader(left, right, width) {
35
- if (!right) return truncateToWidth(left, width);
38
+ if (!right) return truncateToWidth(left, width, '…');
36
39
  const rightWidth = visibleWidth(right);
37
- if (rightWidth + 4 >= width) return truncateToWidth(left, width);
40
+ if (rightWidth + 4 >= width) return truncateToWidth(right, width, '…');
38
41
  const leftWidth = width - rightWidth - 2;
39
- return `${truncateToWidth(left, leftWidth)}${' '.repeat(width - Math.min(visibleWidth(left), leftWidth) - rightWidth)}${right}`;
42
+ const label = truncateToWidth(left, leftWidth, '…');
43
+ return `${label}${' '.repeat(width - visibleWidth(label) - rightWidth)}${right}`;
40
44
  }
41
45
 
42
46
  const escapeMarkdown = text => text.replace(/[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/g, '\\$&');
43
47
  const shortOrigin = origin => origin.split(/[\\/]/).filter(Boolean).at(-1) || origin;
44
48
 
45
- /** The same quiet quote guide in collapsed and expanded states. */
46
- function guidedMarkdown(text, theme, { preview = false, lead } = {}) {
47
- const source = lead ? `**${escapeMarkdown(lead)}** ${text}` : text;
48
- const markdown = new Markdown(source, 0, 0, markdownTheme(theme), { color: fg(theme, 'customMessageText') });
49
- const guide = fg(theme, 'mdQuoteBorder')('│ ');
49
+ /** A final fence also covers very narrow terminals and Markdown tables/code. */
50
+ function bounded(component) {
50
51
  return {
51
- invalidate() { markdown.invalidate(); },
52
- render(width) {
53
- const contentWidth = Math.max(1, width - 2);
54
- let lines = markdown.render(contentWidth);
55
- if (preview) {
56
- const visible = lines.filter(line => line.trim() !== '');
57
- const clipped = visible.length > 2 || text.length >= 1_000;
58
- lines = visible.slice(0, 2);
59
- if (clipped && lines.length) lines[lines.length - 1] = truncateToWidth(`${lines.at(-1).trimEnd()}${fg(theme, 'dim')('…')}`, contentWidth);
60
- }
61
- return lines.map(line => guide + line);
62
- },
52
+ invalidate() { component.invalidate?.(); },
53
+ render(width) { return component.render(Math.max(1, width)).map(line => truncateToWidth(line, Math.max(0, width), '…')); },
63
54
  };
64
55
  }
65
56
 
66
- /** People get the producer's display facts, never the model-facing reminder envelope. */
57
+ /** Human display only: model-facing content and delivery receipts are independent. */
67
58
  export function renderSquareCard(message, { expanded, outputPad = 1 }, theme) {
68
59
  if (typeof message.content !== 'string') return undefined;
69
60
  const items = message.details?.items;
70
61
  if (!Array.isArray(items) || items.length === 0
71
62
  || items.some(item => !isCardPresentation(item) && typeof item?.text !== 'string')) {
72
- return new Markdown(message.content, outputPad, 0, markdownTheme(theme));
63
+ return bounded(new Markdown(message.content, outputPad, 0, markdownTheme(theme)));
73
64
  }
74
65
  const presentations = items.filter(isCardPresentation);
75
- const authors = [...new Set(presentations.flatMap(item => item.author === undefined ? [] : [item.author]))];
76
66
  const origins = [...new Set(presentations.map(item => item.origin))];
77
67
  const originName = origins.map(shortOrigin).join(', ') || 'Notification';
78
- const badge = presentations.at(-1)?.badge;
79
- const byline = [authors.join(', '), badge].filter(Boolean).join(' · ');
80
- const refs = presentations.flatMap(item => item.ref === undefined ? []
81
- : [origins.length > 1 ? `${shortOrigin(item.origin)} ${item.ref}` : item.ref]).join(', ');
82
- const trailing = [refs, presentations.length > 1 ? String(presentations.length) : undefined].filter(Boolean).join(' · ');
68
+ const refs = cardReferences(presentations, shortOrigin);
69
+ const count = items.length > 1 ? fg(theme, 'dim')(` ${items.length} messages`) : '';
70
+ const left = `${fg(theme, 'accent')('■')} ${fg(theme, 'muted')(originName)}${count}`;
71
+ const now = Date.now();
72
+ const bodyColor = fg(theme, 'customMessageText');
73
+ // Restore the body color after each styled attribution inside the Markdown source.
74
+ const bodyPrefix = bodyColor('\0').split('\0')[0];
75
+ const messages = items.map(item => {
76
+ const structured = isCardPresentation(item);
77
+ const label = structured ? reachLabel(item.reach) : undefined;
78
+ const name = structured && item.author !== undefined
79
+ ? fg(theme, 'accent')(style(theme, 'bold')(escapeMarkdown(item.author))) : '';
80
+ const lead = [name, label ? fg(theme, 'warning')(label) : ''].filter(Boolean).join(`${bodyPrefix} `);
81
+ const body = structured ? item.body : item.text;
82
+ // Block Markdown keeps its own first row when there is no paragraph to run into.
83
+ const block = /^(?: {0,3})(?:`{3}|~{3}|#{1,6}\s|>|[-+*]\s|\d+[.)]\s|\|)/.test(body);
84
+ const source = lead ? `${lead}${bodyPrefix}${block ? '\n\n' : ' '}${body}` : body;
85
+ return {
86
+ markdown: new Markdown(source, 0, 0, markdownTheme(theme), { color: bodyColor }),
87
+ guide: fg(theme, label ? 'warning' : 'borderMuted')('│ '),
88
+ time: structured && item.at !== undefined ? timeLabel(item.at, now, true) : '',
89
+ };
90
+ });
91
+ const timeWidth = Math.max(0, ...messages.map(item => visibleWidth(item.time)));
92
+ const gutter = timeWidth ? timeWidth + 2 : 0;
83
93
  const box = new Box(outputPad, 1, text => theme.bg?.('customMessageBg', text) ?? text);
84
- const left = `${fg(theme, 'accent')('■')} ${style(theme, 'bold')(originName)}${byline ? fg(theme, 'muted')(` · ${byline}`) : ''}`;
85
- const right = trailing ? fg(theme, 'dim')(trailing) : '';
86
- box.addChild({ invalidate() {}, render(width) { return [cardHeader(left, right, width)]; } });
87
- const bodyOf = item => isCardPresentation(item) ? item.body : item.text;
88
- if (!expanded) {
89
- const body = bodyOf(items.at(-1));
90
- if (body.trim()) box.addChild(guidedMarkdown(body.slice(0, 1_000).replace(/[\uD800-\uDBFF]$/, ''), theme, { preview: true }));
91
- return box;
92
- }
93
- for (const [index, item] of items.entries()) {
94
- if (index > 0) box.addChild({
95
- invalidate() {},
96
- render(width) { return [`${fg(theme, 'mdQuoteBorder')('│ ')}${fg(theme, 'borderMuted')('─'.repeat(Math.max(1, width - 2)))}`]; },
97
- });
98
- box.addChild(guidedMarkdown(bodyOf(item), theme, {
99
- lead: authors.length > 1 && isCardPresentation(item) ? item.author : undefined,
100
- }));
101
- }
102
- return box;
94
+ box.addChild({
95
+ invalidate() { for (const item of messages) item.markdown.invalidate(); },
96
+ render(width) {
97
+ const bodyWidth = Math.max(1, width - 2 - gutter);
98
+ const lines = messages.flatMap(item => {
99
+ const body = item.markdown.render(bodyWidth);
100
+ // An empty activity can still carry attribution/time.
101
+ if (body.length === 0 && item.time) body.push('');
102
+ return body.map((words, index) => ({ guide: item.guide, words, time: index === 0 ? item.time : '' }));
103
+ });
104
+ const rows = expanded ? lines : lines.filter(line => stripTerminalSequences(line.words).trim() !== '');
105
+ const hidden = expanded ? 0 : Math.max(0, rows.length - 4);
106
+ const visible = (hidden ? rows.slice(0, 4) : rows).map((line, index) => {
107
+ let words = truncateToWidth(line.words, bodyWidth, '…');
108
+ if (hidden && index === 3) {
109
+ words = truncateToWidth(line.words, Math.max(0, bodyWidth - 1), '');
110
+ words = truncateToWidth(words, visibleWidth(stripTerminalSequences(words).trimEnd()), '') + fg(theme, 'dim')('…');
111
+ }
112
+ const time = line.time
113
+ ? ' '.repeat(Math.max(2, width - 2 - visibleWidth(words) - visibleWidth(line.time))) + fg(theme, 'dim')(line.time)
114
+ : '';
115
+ return truncateToWidth(line.guide + words + time, width, '…');
116
+ });
117
+ if (hidden) visible.push(truncateToWidth(fg(theme, 'dim')(` +${hidden} ${hidden === 1 ? 'line' : 'lines'}`), width, '…'));
118
+ return [cardHeader(left, fg(theme, 'dim')(refs), width), ...visible];
119
+ },
120
+ });
121
+ return bounded(box);
103
122
  }
104
123
 
105
124
  export default function squarePiExtension(pi) {
@@ -8,9 +8,9 @@ allowed-tools: Bash(square *)
8
8
 
9
9
  Bring a thought, a question, an objection, or a gesture. Leave private tool chatter out; quiet is welcome too.
10
10
 
11
- This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage. The tools group listen, unlisten and ignore under `attention`.
11
+ This skill explains what Square is and how to be present in it. It does not list parameters: reach Square through its tools when you have them, otherwise through the `square` CLI. The tool descriptions and `square <operation> --help` are the exact usage.
12
12
 
13
- A square is a physical place where participants take in what happens and express thoughts or actions. Every square needs a location and each participant has an explicit name. Tools take the execution body from their live harness context. Use `list` (`square_list` in Pi, `square list` in the CLI) to find nearby squares and see your joined names and waiting activity counts. Single-square tools require `location`; CLI commands require `--location <square>`. Join with an explicit `--as <name>` (tool argument `as`). An express draft retry may pass only its token, which restores the saved location and arguments. Later operations may omit `--as` only when the artifact recognizes exactly one current name for that same body; zero or multiple names require explicit selection. A name never grants custody.
13
+ A square is a physical place where participants take in what happens and express thoughts or actions. Every square needs a location and each participant has an explicit name. Tools take the execution body from their live harness context. Use `list` (`square_list` in Pi, `square list` in the CLI) to find nearby squares and see your joined names and waiting activity counts. Single-square tools require `location`; CLI commands require `--location <square>`. Join with an explicit `--as <name>` (tool argument `as`). Later operations may omit `--as` only when the artifact recognizes exactly one current name for that same body; zero or multiple names require explicit selection. A name never grants custody.
14
14
 
15
15
  ## The rhythm
16
16
 
@@ -1,20 +0,0 @@
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>, signal?: AbortSignal): Promise<void>;
13
- end(signal?: AbortSignal): Promise<ClaudeBoundary | undefined>;
14
- prompt(promptId: string | undefined, signal?: AbortSignal): Promise<void>;
15
- capture(promptId: string, signal?: AbortSignal): Promise<ClaudeBoundary | undefined>;
16
- current(captured: ClaudeBoundary, signal?: AbortSignal): Promise<boolean>;
17
- consume(captured: ClaudeBoundary, signal?: AbortSignal): Promise<void>;
18
- watch<T>(signal: AbortSignal, run: () => Promise<T>): Promise<T>;
19
- };
20
- export {};
@@ -1,84 +0,0 @@
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, signal) {
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: 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, signal) {
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
- }, signal);
51
- },
52
- async end(signal) {
53
- return mutate(async (state, save) => {
54
- if (state)
55
- await save({ ...state, ended: true, stopEpoch: randomUUID() });
56
- return state;
57
- }, signal);
58
- },
59
- async prompt(promptId, signal) {
60
- await mutate(async (state, save) => {
61
- if (state && !state.ended)
62
- await save({ ...state, promptId, stopEpoch: randomUUID(), consumed: false });
63
- }, signal);
64
- },
65
- capture(promptId, signal) {
66
- return mutate(async (state) => state && !state.ended && !state.consumed && state.promptId === promptId ? state : undefined, signal);
67
- },
68
- current(captured, signal) { return mutate(async (state) => matches(state, captured), signal); },
69
- async consume(captured, signal) {
70
- await mutate(async (state, save) => {
71
- if (matches(state, captured))
72
- await save({ ...state, consumed: true });
73
- }, signal);
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
- }
@@ -1,2 +0,0 @@
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>;
@@ -1,232 +0,0 @@
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 signal = AbortSignal.timeout(4500);
57
- const boundaries = claudeBoundaryStore(input.session_id, env);
58
- const mainOwner = ownerFor(input);
59
- const owner = harnessOwner({ channel: 'claude-code', session: input.session_id, ...(input.agent_id === undefined ? {} : { agent: input.agent_id }) });
60
- if (input.hook_event_name === 'SessionStart') {
61
- await boundaries.start(async () => {
62
- const session = await attachDeliverySessionRoutes(mainOwner, env, [{ kind: route, address: { sessionId: input.session_id } }], signal);
63
- if (!session)
64
- return false;
65
- const ledger = await DeliveryLedger.open({ location: deliveryLedgerLocation(env), signal });
66
- try {
67
- await ledger.updateRoutes({ owner: mainOwner, generation: session.generation, routes: session.routes.filter(item => item.kind !== 'claude-native') }, signal);
68
- }
69
- finally {
70
- await ledger.close();
71
- }
72
- return session.generation;
73
- }, signal);
74
- return '';
75
- }
76
- if (input.hook_event_name === 'SessionEnd') {
77
- const captured = await boundaries.end(signal);
78
- if (captured)
79
- await detachDeliverySession(mainOwner, captured.generation, env, signal);
80
- return '';
81
- }
82
- if (input.hook_event_name === 'UserPromptSubmit') {
83
- await boundaries.prompt(input.prompt_id, signal);
84
- if (!input.prompt_id)
85
- return '';
86
- if (input.prompt) {
87
- const ledger = await DeliveryLedger.open({ location: deliveryLedgerLocation(env), signal });
88
- try {
89
- for (const frame of observedFrames(input.prompt)) {
90
- const handoff = await ledger.handoff(frame.token, signal);
91
- if (!handoff || handoff.owner !== owner || handoff.routeKind !== route || handoff.payloadHash !== frame.payloadHash)
92
- continue;
93
- await ledger.observePresentation(handoff, undefined, signal);
94
- }
95
- }
96
- finally {
97
- await ledger.close();
98
- }
99
- }
100
- await presentPendingAtBoundary(owner, async (context) => {
101
- await sink(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: context } })}\n`);
102
- }, env, signal);
103
- return '';
104
- }
105
- if (input.hook_event_name === 'PreToolUse' && input.tool_name?.startsWith('mcp__plugin_square_square__')) {
106
- if (!input.tool_use_id || !input.cwd) {
107
- await denyMcpTool(sink);
108
- return '';
109
- }
110
- try {
111
- await beginClaudeMcpCall({ toolUseId: input.tool_use_id, sessionId: input.session_id, cwd: input.cwd, agentId: input.agent_id }, env, signal);
112
- }
113
- catch {
114
- await denyMcpTool(sink);
115
- return '';
116
- }
117
- }
118
- if ((input.hook_event_name === 'PostToolUse' || input.hook_event_name === 'PostToolUseFailure')
119
- && input.tool_name?.startsWith('mcp__plugin_square_square__') && input.tool_use_id) {
120
- await endClaudeMcpCallForSession(input.tool_use_id, input.session_id, env, input.agent_id, signal).catch(() => undefined);
121
- }
122
- if (input.hook_event_name === 'PostToolUse') {
123
- await presentPendingAtBoundary(owner, async (context) => {
124
- await sink(`${JSON.stringify({ hookSpecificOutput: { hookEventName: input.hook_event_name, additionalContext: context } })}\n`);
125
- }, env, signal);
126
- }
127
- return '';
128
- }
129
- async function denyMcpTool(sink) {
130
- try {
131
- await sink(`${JSON.stringify({ hookSpecificOutput: {
132
- hookEventName: 'PreToolUse', permissionDecision: 'deny',
133
- permissionDecisionReason: 'Square MCP call identity is unavailable; this call did not run.',
134
- } })}\n`);
135
- }
136
- catch {
137
- process.exitCode = 2;
138
- }
139
- }
140
- export async function runClaudeWatch(text, env = process.env, signal, output = frame => new Promise((resolve, reject) => {
141
- process.stderr.write(frame, error => error ? reject(error) : resolve());
142
- })) {
143
- const input = parseInput(text);
144
- if (!input || input.hook_event_name !== 'Stop' || !input.prompt_id)
145
- return '';
146
- const owner = ownerFor(input);
147
- const boundaries = claudeBoundaryStore(input.session_id, env);
148
- const controller = new AbortController();
149
- const timer = setTimeout(() => controller.abort(), 86_400_000);
150
- const stop = () => controller.abort();
151
- process.once('SIGTERM', stop);
152
- process.once('SIGINT', stop);
153
- const combined = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
154
- try {
155
- const captured = await boundaries.capture(input.prompt_id, combined);
156
- if (!captured)
157
- return '';
158
- await boundaries.watch(combined, async () => {
159
- if (!await boundaries.current(captured, combined))
160
- return;
161
- const ledger = await DeliveryLedger.open({ location: deliveryLedgerLocation(env), signal: combined });
162
- const lifetime = retainDeliveryLedger(ledger);
163
- const observer = await ledger.observe(owner);
164
- try {
165
- while (!combined.aborted) {
166
- let version = await observer.read(combined);
167
- const live = await boundaries.current(captured, combined);
168
- const session = await ledger.session(owner, combined);
169
- if (!live || session?.state !== 'attached' || session.generation !== captured.generation || !session.routes.some(item => item.kind === route))
170
- return;
171
- const cards = await ledger.pending(owner, { generation: captured.generation }, combined);
172
- const held = new Set((await ledger.handoffs(owner, { generation: captured.generation }, combined))
173
- .filter(attempt => !['failed', 'released'].includes(attempt.outcome)).map(attempt => attempt.seq));
174
- for (const card of cards) {
175
- if (held.has(card.seq))
176
- continue;
177
- const transport = {
178
- kind: route,
179
- async send(deliveryCard, address, context) {
180
- if (address.sessionId !== input.session_id)
181
- return 'not-capable';
182
- const frame = claudeFrame(deliveryCard, context.token);
183
- const payloadHash = createHash('sha256').update(frame).digest('hex');
184
- if (!await context.prepare({ token: context.token, messageId: context.token, payloadHash }))
185
- return 'not-capable';
186
- const currentSession = await ledger.session(owner, context.signal);
187
- if (currentSession?.state !== 'attached' || currentSession.generation !== captured.generation
188
- || !currentSession.routes.some(item => item.kind === route) || !await context.beforeSend())
189
- return 'not-capable';
190
- const native = await boundaries.current(captured, context.signal);
191
- if (!native || context.signal.aborted || Date.now() >= context.deadline)
192
- return 'not-capable';
193
- try {
194
- await output(frame);
195
- }
196
- finally {
197
- await boundaries.consume(captured, AbortSignal.timeout(5000));
198
- }
199
- process.exitCode = 2;
200
- return 'unknown';
201
- },
202
- };
203
- const result = await ledger.dispatchHandoff(owner, card.key, transport, {
204
- generation: captured.generation, timeoutMs: 5000, signal: combined,
205
- });
206
- lifetime.track(result);
207
- if (result.outcome === 'unknown' || result.outcome === 'accepted' || result.outcome === 'presented')
208
- return;
209
- // Drain our own unsuccessful claim/settlement edge before waiting;
210
- // a not-capable route must not make filesystem hints a retry loop.
211
- version = await observer.read(combined);
212
- }
213
- await observer.changed(version, 1000, combined);
214
- }
215
- }
216
- finally {
217
- observer.close();
218
- await lifetime.close();
219
- }
220
- });
221
- }
222
- catch {
223
- if (!combined.aborted)
224
- return '';
225
- }
226
- finally {
227
- clearTimeout(timer);
228
- process.off('SIGTERM', stop);
229
- process.off('SIGINT', stop);
230
- }
231
- return '';
232
- }
@@ -1,13 +0,0 @@
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, signal?: AbortSignal): Promise<string | undefined>;
@@ -1,75 +0,0 @@
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, signal) {
15
- signal?.throwIfAborted();
16
- const endpoint = typeof options.endpoint === 'string' ? parseLocalEndpoint(options.endpoint) : options.endpoint;
17
- const platform = options.platform ?? process.platform;
18
- const env = options.env ?? process.env;
19
- if (options.token !== undefined) {
20
- if (!validToken(options.token))
21
- throw new DeliveryError('authentication_failed', false);
22
- return options.token;
23
- }
24
- if (validToken(env.CLAUDE_CODE_MESSAGING_TOKEN) && env.CLAUDE_CODE_MESSAGING_SOCKET !== undefined) {
25
- try {
26
- if (parseLocalEndpoint(env.CLAUDE_CODE_MESSAGING_SOCKET).canonical === endpoint.canonical)
27
- return env.CLAUDE_CODE_MESSAGING_TOKEN;
28
- }
29
- catch { /* an unrelated malformed environment coordinate is ignored */ }
30
- }
31
- const home = options.claudeHome ?? env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude');
32
- const digest = createHash('sha256').update(endpoint.canonical).digest('hex');
33
- const keys = [];
34
- try {
35
- for (const name of await readdir(join(home, 'sessions'))) {
36
- signal?.throwIfAborted();
37
- const match = KEY.exec(name);
38
- if (!match || match[2] !== digest)
39
- continue;
40
- try {
41
- keys.push({ name, pid: Number(match[1]), mtimeMs: (await stat(join(home, 'sessions', name))).mtimeMs });
42
- }
43
- catch { /* unreadable entries are skipped */ }
44
- }
45
- }
46
- catch { /* no registry is not an error; it only means no peer key */ }
47
- signal?.throwIfAborted();
48
- keys.sort((a, b) => Number(isAlive(b.pid)) - Number(isAlive(a.pid)) || b.mtimeMs - a.mtimeMs);
49
- for (const key of keys) {
50
- signal?.throwIfAborted();
51
- try {
52
- const value = JSON.parse(await readFile(join(home, 'sessions', key.name), { encoding: 'utf8', signal }));
53
- if (value && typeof value === 'object' && validToken(value.peerToken)) {
54
- return value.peerToken;
55
- }
56
- }
57
- catch { /* the next readable matching key wins */ }
58
- }
59
- signal?.throwIfAborted();
60
- if (platform === 'win32')
61
- throw new DeliveryError('authentication_failed', false);
62
- return undefined;
63
- }
64
- /** A pid that exists, including one owned by another user, holds a live inbox. */
65
- function isAlive(pid) {
66
- if (!Number.isSafeInteger(pid) || pid <= 0)
67
- return false;
68
- try {
69
- process.kill(pid, 0);
70
- return true;
71
- }
72
- catch (error) {
73
- return error.code === 'EPERM';
74
- }
75
- }
@@ -1,30 +0,0 @@
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
- /** Final asynchronous eligibility check after connecting, before writing bytes. */
15
- readonly beforeSend?: () => Promise<boolean>;
16
- }
17
- export type NativeWriteResult = {
18
- readonly outcome: 'written';
19
- readonly message?: never;
20
- } | {
21
- readonly outcome: 'unknown';
22
- readonly code: 'aborted' | 'timeout' | 'transport';
23
- readonly message: string;
24
- } | {
25
- readonly outcome: 'unavailable';
26
- readonly code: 'aborted' | 'timeout' | 'endpoint_unavailable' | 'invalid_arguments' | 'authentication_failed';
27
- readonly message: string;
28
- };
29
- /** Submit once. Written means local bytes only, never native admission or consumption. */
30
- export declare function writeClaudeNative(target: ClaudeNativeTarget, text: string, control: ClaudeNativeControl): Promise<NativeWriteResult>;