@astrosheep/square 0.4.6 → 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 (101) 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 +2 -2
  8. package/dist/cli/context.js +14 -16
  9. package/dist/cli/express-presentation.js +12 -53
  10. package/dist/cli/json.js +2 -2
  11. package/dist/cli/observation-commands.d.ts +2 -7
  12. package/dist/cli/observation-commands.js +31 -97
  13. package/dist/cli/registry.js +3 -6
  14. package/dist/cli/square-commands.d.ts +2 -3
  15. package/dist/cli/square-commands.js +12 -22
  16. package/dist/decisions.js +5 -4
  17. package/dist/harness-claude.js +3 -3
  18. package/dist/help.js +9 -16
  19. package/dist/index.d.ts +1 -1
  20. package/dist/index.js +1 -1
  21. package/dist/mcp.d.ts +1 -0
  22. package/dist/mcp.js +7 -18
  23. package/dist/model.d.ts +1 -1
  24. package/dist/neutral.d.ts +15 -3
  25. package/dist/notifications.d.ts +1 -3
  26. package/dist/notifications.js +21 -23
  27. package/dist/operation-control.d.ts +0 -1
  28. package/dist/operation-control.js +0 -11
  29. package/dist/operation-presentation.d.ts +28 -21
  30. package/dist/operation-presentation.js +90 -87
  31. package/dist/packages/agent-delivery/src/index.d.ts +4 -9
  32. package/dist/packages/agent-delivery/src/index.js +0 -9
  33. package/dist/packages/agent-delivery/src/local-endpoint.d.ts +2 -12
  34. package/dist/packages/agent-delivery/src/local-endpoint.js +4 -48
  35. package/dist/packages/agent-delivery/src/pi-client.js +0 -2
  36. package/dist/packages/agent-delivery/src/types.d.ts +3 -37
  37. package/dist/participant-identity.d.ts +0 -2
  38. package/dist/participant-identity.js +0 -13
  39. package/dist/plugins/paseo/shared/card-format.d.ts +6 -0
  40. package/dist/plugins/paseo/shared/card-format.js +38 -0
  41. package/dist/plugins/paseo/shared/card-presentation.d.ts +3 -1
  42. package/dist/plugins/paseo/shared/card-presentation.js +3 -1
  43. package/dist/presentation.d.ts +5 -22
  44. package/dist/presentation.js +25 -58
  45. package/dist/runtime.d.ts +6 -6
  46. package/dist/runtime.js +5 -15
  47. package/dist/sqlite-square.d.ts +4 -5
  48. package/dist/sqlite-square.js +29 -32
  49. package/dist/square-api.d.ts +1 -1
  50. package/dist/square-api.js +1 -1
  51. package/dist/square-facade.d.ts +5 -14
  52. package/dist/square-location.d.ts +0 -1
  53. package/dist/square-posting.d.ts +4 -4
  54. package/dist/square-posting.js +14 -16
  55. package/dist/square-tools.d.ts +4 -17
  56. package/dist/square-tools.js +52 -119
  57. package/dist/square-values.d.ts +1 -1
  58. package/dist/square-values.js +1 -1
  59. package/dist/time.d.ts +1 -0
  60. package/dist/time.js +10 -0
  61. package/dist/views.d.ts +1 -1
  62. package/dist/views.js +2 -2
  63. package/dist/wake-transports.d.ts +2 -2
  64. package/dist/wake-transports.js +7 -9
  65. package/dist/wakes.js +1 -1
  66. package/examples/symmetric-domains.mjs +1 -2
  67. package/guides/catch-cold-path-optimization.md +1 -1
  68. package/guides/claude-native.md +37 -7
  69. package/guides/delivery-primitives.md +2 -1
  70. package/guides/mcp.md +3 -3
  71. package/guides/pi-native-delivery.md +10 -2
  72. package/guides/pi-tools.md +1 -3
  73. package/guides/row-storage-design.md +1 -1
  74. package/package.json +1 -1
  75. package/plugins/claude/.claude-plugin/plugin.json +3 -2
  76. package/plugins/claude/README.md +9 -0
  77. package/plugins/claude/hooks/hooks.json +1 -9
  78. package/plugins/claude/hooks/register.js +178 -0
  79. package/plugins/claude/skills/square/SKILL.md +2 -2
  80. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  81. package/plugins/paseo/OVERVIEW.md +1 -1
  82. package/plugins/paseo/README.md +20 -23
  83. package/plugins/paseo/client/markdown.tsx +88 -95
  84. package/plugins/paseo/client/palette.ts +37 -12
  85. package/plugins/paseo/client/square-message.tsx +52 -76
  86. package/plugins/paseo/client/transform.ts +4 -6
  87. package/plugins/paseo/shared/card-format.ts +37 -0
  88. package/plugins/paseo/shared/card-presentation.ts +6 -2
  89. package/plugins/paseo/shared/message.ts +1 -6
  90. package/plugins/pi/square-pi.js +76 -60
  91. package/skills/square/SKILL.md +2 -2
  92. package/dist/claude-boundary-state.d.ts +0 -20
  93. package/dist/claude-boundary-state.js +0 -84
  94. package/dist/claude-hook.d.ts +0 -2
  95. package/dist/claude-hook.js +0 -232
  96. package/dist/packages/agent-delivery/src/claude-auth.d.ts +0 -13
  97. package/dist/packages/agent-delivery/src/claude-auth.js +0 -75
  98. package/dist/packages/agent-delivery/src/claude-native.d.ts +0 -30
  99. package/dist/packages/agent-delivery/src/claude-native.js +0 -75
  100. package/dist/packages/agent-delivery/src/claude.d.ts +0 -5
  101. package/dist/packages/agent-delivery/src/claude.js +0 -66
@@ -1,88 +1,64 @@
1
1
  import type { PluginTimelineItemProps } from "@getpaseo/plugin/client";
2
2
  import { Icon } from "@getpaseo/plugin/client/react-native";
3
- import { useMemo, useState } from "react";
3
+ import { useMemo, useRef, useState } from "react";
4
4
  import { Pressable, ScrollView, Text, View } from "react-native";
5
- import { COLLAPSED_PREVIEW_MAX_CHARS, outputPreview, type SquareMessageData } from "../shared/message";
6
- import { MONO, SquareInline, SquareMarkdownBody } from "./markdown";
5
+ import { cardReferences, reachLabel, timeLabel } from "../shared/card-format";
6
+ import type { SquareMessageData } from "../shared/message";
7
+ import { SquareMarkdownBody } from "./markdown";
7
8
  import { squarePalette } from "./palette";
8
9
 
9
- /**
10
- * A matte slate slip in the timeline's shared 4px-corner family: square seal ■,
11
- * thin quote guide under the header.
12
- * Header (both states): ■ origin · authors … refs · how many.
13
- * Collapsed: two preview lines. Expanded: the whole scene, at full width.
14
- * Each structured message keeps its own author, reference and body.
15
- */
10
+ /** The first guide reaches 4px above its text, toward the seal; the collapsed window still shows 4 lines. */
11
+ const GUIDE_LEAD = 4;
12
+ const COLLAPSED_HEIGHT = 4 * 20 + GUIDE_LEAD;
13
+
16
14
  export function SquareMessage({ item, theme, layout }: PluginTimelineItemProps<SquareMessageData>) {
17
15
  const [expanded, setExpanded] = useState(false);
18
16
  const [hovered, setHovered] = useState(false);
17
+ const [overflows, setOverflows] = useState(false);
18
+ const scroll = useRef<ScrollView>(null);
19
19
  const palette = useMemo(() => squarePalette(theme), [theme]);
20
- const items = "items" in item.data ? item.data.items : undefined;
21
- const text = "text" in item.data ? item.data.text : undefined;
22
- const presentations = items?.filter(entry => "origin" in entry);
23
- const latest = items?.at(-1);
24
- // Keep the source's own line breaks: collapsed and expanded text must wrap alike.
25
- const preview = latest && "text" in latest ? outputPreview(latest.text, COLLAPSED_PREVIEW_MAX_CHARS)
26
- : latest?.preview ?? text;
27
- const authors = [...new Set(presentations?.flatMap(entry => entry.author === undefined ? [] : [entry.author]))];
28
- const chorus = authors.length > 1;
29
- const origins = [...new Set(presentations?.map(entry => entry.origin))];
30
- const originName = origins.join(", ") || "Notification";
31
- const badge = presentations?.at(-1)?.badge;
32
- // Exact references never imply intervening messages; each origin keeps its own references.
33
- const trailing = [presentations?.flatMap(entry => entry.ref === undefined ? []
34
- : [origins.length > 1 ? `${entry.origin} ${entry.ref}` : entry.ref]).join(", "),
35
- presentations && presentations.length > 1 ? String(presentations.length) : undefined].filter(Boolean).join(" · ");
36
- const { accent, border, foreground, foregroundMuted: muted, surface1 } = palette.theme.colors;
37
- // On web the header brightens its details under the cursor; touch keeps them quiet.
38
- const detail = layout.platform === "web" && hovered ? foreground : muted;
39
- const row = { flexDirection: "row" as const, alignItems: "center" as const, gap: 6 };
40
- const guide = { marginLeft: 2.5, paddingLeft: palette.quote.paddingLeft, borderLeftWidth: palette.quote.borderLeftWidth, borderLeftColor: palette.quote.borderLeftColor };
20
+ const entries = "items" in item.data ? item.data.items : [{ text: item.data.text }];
21
+ const presentations = entries.filter(entry => "origin" in entry);
22
+ const originName = [...new Set(presentations.map(entry => entry.origin))].join(", ") || "Notification";
23
+ const refs = cardReferences(presentations);
24
+ const { accent, border, foregroundMuted: muted, surface1 } = palette.theme.colors;
25
+ const detail = layout.platform === "web" && hovered ? muted : palette.faint;
41
26
 
42
- return (
43
- <View style={[
44
- { backgroundColor: surface1, borderWidth: palette.card.borderWidth, borderColor: border, borderRadius: palette.card.borderRadius, paddingHorizontal: 10, paddingVertical: 7 },
45
- layout.platform === "web" && { boxShadow: palette.card.boxShadow },
46
- ]}>
47
- <Pressable
48
- accessibilityRole="button"
49
- accessibilityLabel={`${expanded ? "收起" : "展开"} ${originName}: ${authors.join(", ") || "Notification"}`}
50
- aria-expanded={expanded}
51
- onPress={() => setExpanded(value => !value)}
52
- onHoverIn={() => setHovered(true)}
53
- onHoverOut={() => setHovered(false)}
54
- style={({ pressed }) => ({ opacity: pressed ? 0.85 : 1 })}
55
- >
56
- <View style={row}>
57
- <View style={{ width: 6, height: 6, borderRadius: palette.seal.borderRadius, backgroundColor: accent }} />
58
- <Text numberOfLines={1} style={{ color: foreground, fontSize: 11.5, lineHeight: 17, fontWeight: "600", flex: 1 }}>
59
- {originName}
60
- {(authors.length > 0 || badge) && (
61
- <Text style={{ color: muted, fontWeight: "400", fontSize: 10.5 }}> · {[authors.join(", "), badge].filter(Boolean).join(" · ")}</Text>
62
- )}
63
- </Text>
64
- {trailing !== "" && <Text numberOfLines={1} style={{ color: detail, fontSize: 9.5, fontFamily: MONO, fontVariant: ["tabular-nums"], flexShrink: 1 }}>{trailing}</Text>}
65
- <Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={10} color={detail} />
66
- </View>
67
- {!expanded && preview !== undefined && preview !== "" && (
68
- <View style={{ ...guide, marginTop: 5 }}>
69
- <SquareInline text={preview} theme={palette.theme} numberOfLines={2} lead={chorus && latest && "author" in latest ? latest.author : undefined} />
70
- </View>
71
- )}
72
- </Pressable>
73
- {expanded && (
74
- <ScrollView style={{ maxHeight: layout.compact ? 280 : 400, marginTop: 5 }} nestedScrollEnabled>
75
- <View style={{ ...guide, gap: 7 }}>
76
- {items === undefined ? <SquareMarkdownBody text={text ?? ""} theme={palette.theme} /> : items.map((entry, index) => (
77
- <View key={index} style={index > 0 ? { paddingTop: 7, borderTopWidth: palette.card.borderWidth, borderTopColor: border } : undefined}>
78
- {"text" in entry
79
- ? <SquareMarkdownBody text={entry.text} theme={palette.theme} />
80
- : <SquareMarkdownBody text={entry.body} theme={palette.theme} lead={chorus ? entry.author : undefined} />}
81
- </View>
82
- ))}
83
- </View>
84
- </ScrollView>
85
- )}
27
+ return <Pressable onPress={() => {
28
+ scroll.current?.scrollTo({ y: 0, animated: false });
29
+ setExpanded(value => !value);
30
+ }} accessibilityRole="button"
31
+ accessibilityLabel={`${expanded ? "Collapse" : "Expand"} ${originName}`} aria-expanded={expanded}
32
+ onHoverIn={() => setHovered(true)} onHoverOut={() => setHovered(false)}
33
+ style={{ backgroundColor: surface1, borderWidth: palette.card.borderWidth,
34
+ borderColor: border, borderRadius: palette.card.borderRadius, paddingVertical: 9, paddingHorizontal: 12 }}>
35
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 7, height: 16 }}>
36
+ <View style={{ width: 5, height: 5, backgroundColor: accent }} />
37
+ <Text numberOfLines={1} style={{ color: muted, fontSize: 11, fontWeight: "500", flexShrink: 1 }}>{originName}</Text>
38
+ {entries.length > 1 && <Text style={{ color: detail, fontSize: 11 }}>{entries.length} messages</Text>}
39
+ <View style={{ flex: 1 }} />
40
+ {!!refs && <Text numberOfLines={1} style={{ color: detail, fontSize: 11, fontVariant: ["tabular-nums"], flexShrink: 1 }}>{refs}</Text>}
41
+ <View style={{ width: 10, height: 10 }}>
42
+ {overflows && <Icon name={expanded ? "ChevronUp" : "ChevronDown"} size={10} color={detail} />}
43
+ </View>
44
+ </View>
45
+ <View style={{ marginTop: 8 - GUIDE_LEAD, marginLeft: 2 }}>
46
+ <ScrollView ref={scroll} style={{ maxHeight: expanded ? 260 : COLLAPSED_HEIGHT }} scrollEnabled={expanded} nestedScrollEnabled
47
+ onContentSizeChange={(_, height) => setOverflows(height > COLLAPSED_HEIGHT)}>
48
+ {entries.map((entry, index) => {
49
+ const structured = "origin" in entry;
50
+ // A mention speaks through the guide colour alone; reply and bell keep their word.
51
+ const direct = structured && reachLabel(entry.reach) !== undefined;
52
+ const reach = structured && entry.reach !== "mention" ? reachLabel(entry.reach) : undefined;
53
+ return <View key={index} style={{ borderLeftWidth: 1,
54
+ borderLeftColor: direct ? palette.directGuide : palette.quote.borderLeftColor,
55
+ paddingLeft: palette.quote.paddingLeft, marginTop: index > 0 ? 8 : 0, paddingTop: index > 0 ? 0 : GUIDE_LEAD }}>
56
+ <SquareMarkdownBody text={structured ? entry.body : entry.text} palette={palette}
57
+ author={structured ? entry.author : undefined} reach={reach}
58
+ time={structured && entry.at !== undefined ? timeLabel(entry.at) : undefined} />
59
+ </View>;
60
+ })}
61
+ </ScrollView>
86
62
  </View>
87
- );
63
+ </Pressable>;
88
64
  }
@@ -1,5 +1,5 @@
1
1
  import type { PluginTimelineTransformerContribution } from "@getpaseo/plugin/client";
2
- import { COLLAPSED_PREVIEW_MAX_CHARS, OUTPUT_DISPLAY_MAX_CHARS, outputPreview, squareCardDetailsSchema, type SquareMessageData } from "../shared/message.ts";
2
+ import { OUTPUT_DISPLAY_MAX_CHARS, outputPreview, squareCardDetailsSchema, type SquareMessageData } from "../shared/message.ts";
3
3
 
4
4
  // Recognition is metadata-based, not a guess from a tool name or body text.
5
5
  // The same projection applies to live events and fetched history.
@@ -12,17 +12,15 @@ export const transformSquareMessage: PluginTimelineTransformerContribution<"tool
12
12
  const details = squareCardDetailsSchema.safeParse(item.metadata.details);
13
13
  let data: SquareMessageData;
14
14
  if (details.success) {
15
- const items = details.data.items, latest = items.at(-1)!;
16
- const preview = outputPreview("text" in latest ? latest.text : latest.preview ?? latest.body, COLLAPSED_PREVIEW_MAX_CHARS);
17
- let remaining = OUTPUT_DISPLAY_MAX_CHARS - preview.length;
15
+ const items = details.data.items;
16
+ let remaining = OUTPUT_DISPLAY_MAX_CHARS;
18
17
  const clipped = items.reduce((sum, entry) => sum + ("text" in entry ? entry.text : entry.body).length, 0) > remaining;
19
18
  data = { items: items.map((entry, index) => {
20
19
  const source = "text" in entry ? entry.text : entry.body;
21
20
  const body = clipped ? outputPreview(source, Math.floor(remaining / (items.length - index))) : source;
22
21
  remaining -= body.length;
23
22
  if ("text" in entry) return { text: body };
24
- const { preview: _preview, ...presentation } = entry;
25
- return { ...presentation, body, ...(index === items.length - 1 ? { preview } : {}) };
23
+ return { ...entry, body };
26
24
  }) };
27
25
  } else {
28
26
  data = { text: outputPreview(item.detail.text) };
@@ -0,0 +1,37 @@
1
+ import type { CardPresentation } from "./card-presentation.ts";
2
+
3
+ export function reachLabel(reach?: CardPresentation["reach"]): string | undefined {
4
+ return reach === "mention" ? "@you" : reach === "attention" ? undefined : reach;
5
+ }
6
+
7
+ /** Calendar days, rather than elapsed hours, keep yesterday correct across DST. */
8
+ export function timeLabel(at: number, now = Date.now(), compact = false): string {
9
+ const date = new Date(at), today = new Date(now);
10
+ const day = (value: Date) => `${value.getFullYear()}-${value.getMonth()}-${value.getDate()}`;
11
+ const clock = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
12
+ if (day(date) === day(today)) return clock;
13
+ const yesterday = new Date(today);
14
+ yesterday.setDate(today.getDate() - 1);
15
+ if (day(date) === day(yesterday)) return `${compact ? "yest" : "yesterday"} ${clock}`;
16
+ const shortDate = compact
17
+ ? `${String(date.getMonth() + 1).padStart(2, "0")}/${String(date.getDate()).padStart(2, "0")}`
18
+ : date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
19
+ return `${shortDate} ${clock}`;
20
+ }
21
+
22
+ /** Only adjacent references from the same origin can form a range. */
23
+ export function cardReferences(items: readonly CardPresentation[], originLabel: (origin: string) => string = origin => origin): string {
24
+ const multipleOrigins = new Set(items.map(item => item.origin)).size > 1;
25
+ const runs: { origin: string; ref: string; first?: number; last?: number }[] = [];
26
+ for (const item of items) {
27
+ if (item.ref === undefined) continue;
28
+ const match = /^act\/(\d+)$/.exec(item.ref);
29
+ const index = match ? Number(match[1]) : undefined;
30
+ const previous = runs.at(-1);
31
+ if (index !== undefined && previous?.origin === item.origin && previous.last !== undefined && previous.last + 1 === index) {
32
+ previous.last = index;
33
+ } else runs.push({ origin: item.origin, ref: item.ref, first: index, last: index });
34
+ }
35
+ return runs.map(run => `${multipleOrigins ? `${originLabel(run.origin)} ` : ""}${run.first !== run.last
36
+ ? `act/${run.first}–${run.last}` : run.ref}`).join(", ");
37
+ }
@@ -4,7 +4,9 @@ export type CardPresentation = {
4
4
  readonly origin: string;
5
5
  readonly author?: string;
6
6
  readonly ref?: string;
7
- readonly badge?: string;
7
+ /** Absent on cards produced before landing time and reach were recorded. */
8
+ readonly at?: number;
9
+ readonly reach?: 'mention' | 'reply' | 'attention' | 'bell';
8
10
  readonly body: string;
9
11
  };
10
12
 
@@ -12,5 +14,7 @@ export function isCardPresentation(value: unknown): value is CardPresentation {
12
14
  if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
13
15
  const item = value as Record<string, unknown>;
14
16
  return item.kind === 'presentation' && typeof item.origin === 'string' && typeof item.body === 'string'
15
- && ['author', 'ref', 'badge'].every(key => item[key] === undefined || typeof item[key] === 'string');
17
+ && (item.at === undefined || (typeof item.at === 'number' && Number.isFinite(item.at)))
18
+ && (item.reach === undefined || ['mention', 'reply', 'attention', 'bell'].includes(item.reach as string))
19
+ && ['author', 'ref'].every(key => item[key] === undefined || typeof item[key] === 'string');
16
20
  }
@@ -3,12 +3,10 @@ import { isCardPresentation, type CardPresentation } from "./card-presentation.t
3
3
 
4
4
  /** Aggregate client display budget, including omission lines; source history stays unchanged. */
5
5
  export const OUTPUT_DISPLAY_MAX_CHARS = 32_000;
6
- export const COLLAPSED_PREVIEW_MAX_CHARS = 1_000;
7
6
  export function outputPreview(text: string, maxChars = OUTPUT_DISPLAY_MAX_CHARS): string {
8
7
  if (maxChars <= 0) return "";
9
8
  if (text.length <= maxChars) return text;
10
9
  const middle = "\n\n… Showing the beginning and end; the middle is hidden in this preview. …\n\n";
11
- // Even a small share of a multi-message budget must fit, without splitting a surrogate pair.
12
10
  if (maxChars <= middle.length) return text.slice(0, maxChars - 1).replace(/[\uD800-\uDBFF]$/, "") + "…";
13
11
  const headChars = Math.floor((maxChars - middle.length) / 2);
14
12
  const head = text.slice(0, headChars).replace(/[\uD800-\uDBFF]$/, "");
@@ -16,13 +14,10 @@ export function outputPreview(text: string, maxChars = OUTPUT_DISPLAY_MAX_CHARS)
16
14
  return head + middle + tail;
17
15
  }
18
16
 
19
- /** Receiver display facts, with a bounded preview derived on the client. */
20
- const presentationItemSchema = z.custom<CardPresentation>(isCardPresentation)
21
- .and(z.object({ preview: z.string().optional() }));
17
+ const presentationItemSchema = z.custom<CardPresentation>(isCardPresentation);
22
18
  export const squareCardDetailsSchema = z.object({
23
19
  items: z.array(z.union([presentationItemSchema, z.object({ text: z.string() })])).min(1),
24
20
  });
25
-
26
21
  export const squareMessageSchema = z.union([
27
22
  squareCardDetailsSchema,
28
23
  z.object({ text: z.string() }),
@@ -1,14 +1,14 @@
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
- import { Square, discover, squareClock } from '../../dist/square-api.js';
5
+ import { Square, discover } from '../../dist/square-api.js';
5
6
  import { harnessOwner } from '../../dist/owner.js';
6
7
  import { DeliveryLedger } from '../../dist/delivery-ledger.js';
7
8
  import { deliveryFrameOverhead, renderDeliveryReminder } from '../../dist/delivery-frame.js';
8
9
  import { isCardPresentation } from '../../dist/card-presentation.js';
9
10
  import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, createSquareToolRunner, squareToolsEnabled } from '../../dist/square-tools.js';
10
- import { operationEnv } from '../../dist/participant-identity.js';
11
- import { attachDeliverySession, detachDeliverySession } from '../../dist/session-lifecycle.js';
11
+ import { attachDeliverySessionRoutes, detachDeliverySession } from '../../dist/session-lifecycle.js';
12
12
  import { createPiReceiver, sendPiMessage } from '../../dist/packages/agent-delivery/src/pi.js';
13
13
 
14
14
  const BUDGET_CHARS = 12_000;
@@ -26,81 +26,99 @@ function markdownTheme(theme) {
26
26
  heading: color('mdHeading'), link: color('mdLink'), linkUrl: color('mdLinkUrl'), code: color('mdCode'),
27
27
  codeBlock: color('mdCodeBlock'), codeBlockBorder: color('mdCodeBlockBorder'), quote: color('mdQuote'),
28
28
  quoteBorder: color('mdQuoteBorder'), hr: color('mdHr'), listBullet: color('mdListBullet'),
29
- 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, ''))),
30
32
  underline: style(theme, 'underline'), strikethrough: style(theme, 'strikethrough'),
31
33
  };
32
34
  }
33
35
 
34
- /** 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. */
35
37
  function cardHeader(left, right, width) {
36
- if (!right) return truncateToWidth(left, width);
38
+ if (!right) return truncateToWidth(left, width, '…');
37
39
  const rightWidth = visibleWidth(right);
38
- if (rightWidth + 4 >= width) return truncateToWidth(left, width);
40
+ if (rightWidth + 4 >= width) return truncateToWidth(right, width, '…');
39
41
  const leftWidth = width - rightWidth - 2;
40
- 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}`;
41
44
  }
42
45
 
43
46
  const escapeMarkdown = text => text.replace(/[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/g, '\\$&');
44
47
  const shortOrigin = origin => origin.split(/[\\/]/).filter(Boolean).at(-1) || origin;
45
48
 
46
- /** The same quiet quote guide in collapsed and expanded states. */
47
- function guidedMarkdown(text, theme, { preview = false, lead } = {}) {
48
- const source = lead ? `**${escapeMarkdown(lead)}** ${text}` : text;
49
- const markdown = new Markdown(source, 0, 0, markdownTheme(theme), { color: fg(theme, 'customMessageText') });
50
- const guide = fg(theme, 'mdQuoteBorder')('│ ');
49
+ /** A final fence also covers very narrow terminals and Markdown tables/code. */
50
+ function bounded(component) {
51
51
  return {
52
- invalidate() { markdown.invalidate(); },
53
- render(width) {
54
- const contentWidth = Math.max(1, width - 2);
55
- let lines = markdown.render(contentWidth);
56
- if (preview) {
57
- const visible = lines.filter(line => line.trim() !== '');
58
- const clipped = visible.length > 2 || text.length >= 1_000;
59
- lines = visible.slice(0, 2);
60
- if (clipped && lines.length) lines[lines.length - 1] = truncateToWidth(`${lines.at(-1).trimEnd()}${fg(theme, 'dim')('…')}`, contentWidth);
61
- }
62
- return lines.map(line => guide + line);
63
- },
52
+ invalidate() { component.invalidate?.(); },
53
+ render(width) { return component.render(Math.max(1, width)).map(line => truncateToWidth(line, Math.max(0, width), '…')); },
64
54
  };
65
55
  }
66
56
 
67
- /** 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. */
68
58
  export function renderSquareCard(message, { expanded, outputPad = 1 }, theme) {
69
59
  if (typeof message.content !== 'string') return undefined;
70
60
  const items = message.details?.items;
71
61
  if (!Array.isArray(items) || items.length === 0
72
62
  || items.some(item => !isCardPresentation(item) && typeof item?.text !== 'string')) {
73
- return new Markdown(message.content, outputPad, 0, markdownTheme(theme));
63
+ return bounded(new Markdown(message.content, outputPad, 0, markdownTheme(theme)));
74
64
  }
75
65
  const presentations = items.filter(isCardPresentation);
76
- const authors = [...new Set(presentations.flatMap(item => item.author === undefined ? [] : [item.author]))];
77
66
  const origins = [...new Set(presentations.map(item => item.origin))];
78
67
  const originName = origins.map(shortOrigin).join(', ') || 'Notification';
79
- const badge = presentations.at(-1)?.badge;
80
- const byline = [authors.join(', '), badge].filter(Boolean).join(' · ');
81
- const refs = presentations.flatMap(item => item.ref === undefined ? []
82
- : [origins.length > 1 ? `${shortOrigin(item.origin)} ${item.ref}` : item.ref]).join(', ');
83
- 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;
84
93
  const box = new Box(outputPad, 1, text => theme.bg?.('customMessageBg', text) ?? text);
85
- const left = `${fg(theme, 'accent')('■')} ${style(theme, 'bold')(originName)}${byline ? fg(theme, 'muted')(` · ${byline}`) : ''}`;
86
- const right = trailing ? fg(theme, 'dim')(trailing) : '';
87
- box.addChild({ invalidate() {}, render(width) { return [cardHeader(left, right, width)]; } });
88
- const bodyOf = item => isCardPresentation(item) ? item.body : item.text;
89
- if (!expanded) {
90
- const body = bodyOf(items.at(-1));
91
- if (body.trim()) box.addChild(guidedMarkdown(body.slice(0, 1_000).replace(/[\uD800-\uDBFF]$/, ''), theme, { preview: true }));
92
- return box;
93
- }
94
- for (const [index, item] of items.entries()) {
95
- if (index > 0) box.addChild({
96
- invalidate() {},
97
- render(width) { return [`${fg(theme, 'mdQuoteBorder')('│ ')}${fg(theme, 'borderMuted')('─'.repeat(Math.max(1, width - 2)))}`]; },
98
- });
99
- box.addChild(guidedMarkdown(bodyOf(item), theme, {
100
- lead: authors.length > 1 && isCardPresentation(item) ? item.author : undefined,
101
- }));
102
- }
103
- 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);
104
122
  }
105
123
 
106
124
  export default function squarePiExtension(pi) {
@@ -116,13 +134,10 @@ export default function squarePiExtension(pi) {
116
134
  async execute(_id, args, signal, _onUpdate, ctx) {
117
135
  const { text, isError } = await invoke(tool, args, { signal }, () => {
118
136
  const sessionId = ctx.sessionManager.getSessionId();
119
- const env = operationEnv('pi', sessionId, process.env);
120
137
  return {
121
138
  identity: { owner: harnessOwner({ channel: 'pi', session: sessionId }) },
122
- open: (location, control) => Square.at({
123
- cwd: ctx.cwd, env, path: location,
124
- }, control),
125
- discover: async (options, control) => ({ result: await discover({ cwd: ctx.cwd, env }, options, control), now: squareClock(env)() }),
139
+ open: (location, control) => Square.at({ cwd: ctx.cwd, path: location }, control),
140
+ discover: (options, control) => discover({ cwd: ctx.cwd }, options, control),
126
141
  };
127
142
  });
128
143
  return { ...(isError ? { isError } : {}), content: [{ type: 'text', text }] };
@@ -245,9 +260,10 @@ export default function squarePiExtension(pi) {
245
260
  await previous.ledger.close();
246
261
  }
247
262
  if (token !== epoch || cancellationFailed) return;
248
- const owner = harnessOwner({ channel: 'pi', session: ctx.sessionManager.getSessionId() });
263
+ const sessionId = ctx.sessionManager.getSessionId();
264
+ const owner = harnessOwner({ channel: 'pi', session: sessionId });
249
265
  const signal = AbortSignal.timeout(5000);
250
- const attached = await attachDeliverySession(owner, process.env, undefined, signal);
266
+ const attached = await attachDeliverySessionRoutes(owner, process.env, [{ kind: 'pi-native', address: { sessionId } }], signal);
251
267
  if (!attached) return;
252
268
  if (token !== epoch) {
253
269
  await detachDeliverySession(owner, attached.generation, process.env, AbortSignal.timeout(5000));
@@ -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>;