@astrosheep/square 0.3.76 → 0.3.78
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/attention-presentation.d.ts +1 -1
- package/dist/attention-presentation.js +7 -10
- package/dist/boundary-presentation.js +3 -2
- package/dist/claude-mcp-context.d.ts +15 -0
- package/dist/claude-mcp-context.js +82 -0
- package/dist/claude-mod.js +8 -0
- package/dist/cli/context.js +0 -1
- package/dist/cli/maintenance-commands.js +2 -2
- package/dist/cli/program.js +8 -2
- package/dist/host-ledger-migration.js +2 -6
- package/dist/host-ledger-sqlite.js +2 -2
- package/dist/mcp.js +28 -57
- package/dist/model.d.ts +2 -0
- package/dist/square-sqlite.d.ts +2 -8
- package/dist/square-sqlite.js +3 -11
- package/dist/square-tools.d.ts +14 -0
- package/dist/square-tools.js +50 -0
- package/guides/application-facade-design.md +4 -1
- package/guides/claude-native.md +32 -1
- package/guides/mcp-connection-lifetime.md +1 -1
- package/guides/mcp.md +5 -1
- package/guides/migration.md +2 -1
- package/guides/pi-tools.md +21 -0
- package/guides/row-storage-design.md +1 -1
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/.mcp.json +11 -0
- package/plugins/claude/hooks/register.js +10 -24
- package/plugins/claude/skills/square/SKILL.md +29 -100
- package/plugins/claude/types.d.ts +0 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/square-pi.js +95 -25
- package/skills/square/SKILL.md +29 -100
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# MCP connection lifetime
|
|
2
2
|
|
|
3
|
-
An MCP server owns one artifact connection for its configured square. Its lifetime ends on close or disconnect; create a new server instance for a new connection. Replacing an artifact file under a running server remains unsupported. Every tool call still creates an application with that request's participant and harness session. Only the connection is shared: authorization, presence, activity facts and cancellation remain operation-scoped. The artifact port already serializes transactions on its connection.
|
|
3
|
+
An MCP server owns one artifact connection for its configured square. Its lifetime ends on close or disconnect; create a new server instance for a new connection. Replacing an artifact file under a running server remains unsupported. Every tool call still creates an application with that request's participant and harness session. Calls with an explicit `location` override instead open and close a per-call connection, leaving the configured-square owner untouched. Only the default connection is shared: authorization, presence, activity facts and cancellation remain operation-scoped. The artifact port already serializes transactions on its connection.
|
|
4
4
|
|
|
5
5
|
`createSquareResourceOwner()` is the explicit lifetime boundary. `scoped()` borrows its connection; ordinary application callers still open and close their own. Cross-square `inbox()` reads remain independent. The connection captures no request abort signal. Explicit server close and transport disconnect share one shutdown path: the SDK cancels requests, handlers finish their cleanup, then the connection closes.
|
|
6
6
|
|
package/guides/mcp.md
CHANGED
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Square provides a stdio MCP server through `square mcp-server`.
|
|
4
4
|
|
|
5
|
-
Set `SQUARE_LOCATION` to the `.square` artifact path and `SQUARE_PARTICIPANT_NAME` to the participant
|
|
5
|
+
Set `SQUARE_LOCATION` to the default `.square` artifact path and optionally `SQUARE_PARTICIPANT_NAME` to the default participant. Calls may override these with `location` and `as`; relative paths use the server's startup working directory. Request metadata supplies the live Codex (`threadId`) or OpenCode (`ai.opencode/sessionID`) identity when present, otherwise the startup environment is used. Participant names are not session identities.
|
|
6
6
|
|
|
7
7
|
The server exposes `join`, `express`, `catch`, `history`, `listen`, `ignore`, `listening`, `hold`, `resume`, `done`, `status`, and `participants`. `catch` consumes activities. `history` is read-only and accepts stable `act/<index>` cursors. Tool results include JSON `structuredContent` and a text representation of the same data. Square failures are returned as tool errors with a structured `{ error: { code, message, facts? } }` value.
|
|
8
8
|
|
|
9
|
+
Claude's bundled plugin configures this server automatically. Its mod supplies live session/cwd/subagent attribution through an out-of-band tool-use-ID association, not AI-visible arguments; see [Claude call identity](claude-native.md#mcp-tools-and-call-identity). The bundled endpoint requires that association rather than falling back to its startup environment.
|
|
10
|
+
|
|
11
|
+
Tool definitions and argument-to-facade calls live in `square-tools.ts`, shared with the [Pi native tools](pi-tools.md). `express.noMention` disables body @name scanning without disabling explicit mentions or reply attention.
|
|
12
|
+
|
|
9
13
|
Configure a client with command `npx` and arguments `--yes --package @astrosheep/square square mcp-server`, or use the installed `square` executable with argument `mcp-server`. The MCP server writes protocol messages only to stdout; diagnostics go to stderr.
|
package/guides/migration.md
CHANGED
|
@@ -6,7 +6,8 @@ sparse receipts, catch leases, ordered routes and the small configuration
|
|
|
6
6
|
facts — in one in-place database.
|
|
7
7
|
|
|
8
8
|
Existing artifacts are **not** converted automatically. A normal read of a
|
|
9
|
-
version 1 artifact refuses with
|
|
9
|
+
version 1 artifact refuses with a descriptive offline-migration requirement.
|
|
10
|
+
The CLI adds the concrete recovery command; native tools retain the description:
|
|
10
11
|
|
|
11
12
|
```sh
|
|
12
13
|
square --location /path/to/SQUARE.square migrate
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Pi native tools
|
|
2
|
+
|
|
3
|
+
The existing Square Pi extension registers `square_join`, `square_express`, `square_catch`, `square_history`, `square_listen`, `square_ignore`, `square_listening`, `square_hold`, `square_resume`, `square_done`, `square_status`, and `square_participants`. No additional MCP server or subprocess is needed.
|
|
4
|
+
|
|
5
|
+
Tools use the `square` namespace with codemode exposure. Enable Pi's codemode tool (`pi --tools +codemode`) and inspect them with `describeNamespace('square')`. They can also be explicitly activated as direct tools using Pi's tool selection.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
const result = await tools.square_express({
|
|
9
|
+
location: '/path/to/conversation.square',
|
|
10
|
+
as: 'rei',
|
|
11
|
+
body: 'A different angle on this.',
|
|
12
|
+
mentions: ['aoi'],
|
|
13
|
+
});
|
|
14
|
+
text(result);
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`location` defaults to `SQUARE_LOCATION`; relative paths use the current call's cwd. `as` defaults to the configured participant or the current session's unambiguous participant. The extension supplies the live Pi session identity, never a model-provided session id, without mutating the process environment.
|
|
18
|
+
|
|
19
|
+
Codemode receives the operation's data object directly. Failures return `{ error: { code, message, facts?, operation? } }` and are marked as failed tool results; scripts must inspect `error`. `catch` consumes activity; `history` does not. Exact parameters and semantics come from the tool descriptions, shared with MCP through `square-tools.ts`.
|
|
20
|
+
|
|
21
|
+
Each operation owns and closes its connection through the facade, including cancellation cleanup. Existing notification delivery, rendering and session lifecycle stay independent. Reload the extension after installing an updated package to register the tools in an existing Pi session.
|
|
@@ -465,7 +465,7 @@ These terms apply together with the domain specification above.
|
|
|
465
465
|
## Migration protocol
|
|
466
466
|
|
|
467
467
|
- Only `square --location <path.square> migrate`. No --check, --backup, --force, auto-migrate, bulk mode, separate lock database, sidecar, daemon, or runtime dual reader. An operator can make an offline backup with ordinary tooling; the command does not invent a backup lifecycle.
|
|
468
|
-
- application_id remains 0x53515245; user_version 2 identifies rows. Normal new-format reads/writes refuse version 1 with invalid_args / reason migration_required and
|
|
468
|
+
- application_id remains 0x53515245; user_version 2 identifies rows. Normal new-format reads/writes refuse version 1 with invalid_args / reason migration_required and descriptive offline-migration guidance. The CLI adds the shell-quoted recovery command; native tool errors do not embed invocation syntax. build --force does not bypass it. Unknown/unrelated/invalid formats are refused, not repaired.
|
|
469
469
|
- `doctor` remains read-only: recognizes legacy v1 and prints the migration recovery command (exit 2); validates v2 fully, including a replay comparison of derived indices. It must not imply that legacy v1 is damaged. Directory/list probing retains omission semantics for unsupported files; no new implicit migration/discovery subsystem.
|
|
470
470
|
- Open existing canonical path create-disabled. A read-only preflight establishes owned known schema/version before any persistent pragma or DDL. Require existing DELETE journal mode, refuse another mode without changing it; the shipped v1 creator/writer already establishes DELETE. Set connection busy_timeout=0 and synchronous=FULL. Acquire BEGIN IMMEDIATE, re-check format/schema under the lock, and validate the full v1 snapshot inside it. If v2 is already present and has valid framing/header, return already-format-2 without changing revision. A second migrator follows this same in-lock check.
|
|
471
471
|
- Use v1 validation retained ONLY in migration (test oracle may also retain it), plus one explicit restriction: non-finite numbers anywhere in a route (including nested extras) are unsupported input, refused before DDL/inserts with the artifact untouched. They are not normally persisted as non-finite values by the v1 JSON.stringify writer; do not add a bespoke overflow-number codec or silently coerce to null. Copy normally persisted authoritative facts losslessly, preserving revision and nextActIndex, activity indices/gaps and optional field distinctions, raw receipt/lease owner keys, route order/duplicates, strings and finite fractional timestamps. Absent routes and [] both become zero rows; empty receipt-owner maps are behaviorless and may become zero receipt rows. Do not canonicalize receipts or add foreign keys that reject accepted v1 facts.
|
package/package.json
CHANGED
|
@@ -55,15 +55,6 @@ async function cancel($) {
|
|
|
55
55
|
|
|
56
56
|
export function register(on) {
|
|
57
57
|
on('session.start', async ($, e, next) => {
|
|
58
|
-
// Claude refuses plugin tools in sessions started without built-in tools; the inbox must still reconcile.
|
|
59
|
-
try {
|
|
60
|
-
await $.tool.register({
|
|
61
|
-
name: 'square',
|
|
62
|
-
description: "Run the square CLI. args are exactly the words after `square` in the square skill's commands.",
|
|
63
|
-
inputSchema: { type: 'object', properties: { args: { type: 'array', items: { type: 'string' } }, stdin: { type: 'string' } }, required: ['args'] },
|
|
64
|
-
isDeferred: false,
|
|
65
|
-
});
|
|
66
|
-
} catch (error) { await $.ui.log(`Square tool unavailable: ${error?.message ?? error}`); }
|
|
67
58
|
await reconcile($).catch(() => undefined);
|
|
68
59
|
return next(e);
|
|
69
60
|
});
|
|
@@ -73,23 +64,18 @@ export function register(on) {
|
|
|
73
64
|
});
|
|
74
65
|
on('tool.call', async ($, e, next) => {
|
|
75
66
|
try {
|
|
76
|
-
if (e.tool !== '
|
|
77
|
-
const sessionId = await $.session.id();
|
|
78
|
-
const cwd = await $.session.cwd();
|
|
79
|
-
// process.run has no signal option; it belongs to this cancellable dispatch.
|
|
67
|
+
if (typeof e.tool !== 'string' || !e.tool.startsWith('mcp__plugin_square_square__')) return await next(e);
|
|
80
68
|
next.signal.throwIfAborted();
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return { deny: text };
|
|
69
|
+
const c = await coordinate($);
|
|
70
|
+
const call = { ...c, toolUseId: e.tool_use_id, agentId: e.agentId };
|
|
71
|
+
const { token } = await bridge($, { ...call, operation: 'tool-begin' });
|
|
72
|
+
try {
|
|
73
|
+
next.signal.throwIfAborted();
|
|
74
|
+
// MCP receives the original business arguments; identity travels only by tool-use ID.
|
|
75
|
+
return await next(e);
|
|
76
|
+
} finally {
|
|
77
|
+
await bridge($, { ...call, token, operation: 'tool-end' }).catch(() => undefined);
|
|
91
78
|
}
|
|
92
|
-
return { result: text };
|
|
93
79
|
}
|
|
94
80
|
finally { if (!e.agentId) await reconcile($, undefined, 'reconcile').catch(() => undefined); }
|
|
95
81
|
});
|
|
@@ -6,137 +6,66 @@ allowed-tools: Bash(square *)
|
|
|
6
6
|
|
|
7
7
|
# Square
|
|
8
8
|
|
|
9
|
-
When a `square` tool is available, call it with the same arguments instead of the shell; pass any `-` body as `stdin`.
|
|
10
|
-
|
|
11
9
|
Don't treat Square as a public dumping ground. Speak here only when you genuinely think someone else needs to know; otherwise you are needlessly interrupting them.
|
|
12
10
|
|
|
13
|
-
|
|
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 operations are the same either way.
|
|
12
|
+
|
|
13
|
+
A square is a physical place where participants catch up and express thoughts or actions. Every square needs a location and a participant name. Tools take them from your session; the CLI takes `--location` and `--as`, or `SQUARE_LOCATION` and `SQUARE_PARTICIPANT_NAME`.
|
|
14
|
+
|
|
15
|
+
## The rhythm
|
|
14
16
|
|
|
15
17
|
```text
|
|
16
18
|
PUBLIC.square : catch ↔ express directly (no join)
|
|
17
|
-
other square :
|
|
18
|
-
|
|
19
|
+
other square : list → join once → catch ↔ express
|
|
20
|
+
└→ history to look back
|
|
19
21
|
leave for good : done (permanent — not the end of a round)
|
|
20
22
|
```
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
`.square/PUBLIC.square` is the public square. You do not need to `join` it — catch up and participate directly:
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
square --location .square/PUBLIC.square --as <name> catch
|
|
28
|
-
square --location .square/PUBLIC.square --as <name> express --mention alice "your thought"
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
For any other square, find it, then `join` it once (`ls` is short for `list`; `--depth N` widens the search):
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
square ls
|
|
35
|
-
square --location <square> --as <name> join
|
|
36
|
-
```
|
|
24
|
+
`.square/PUBLIC.square` is the public square; catch up and participate there directly. Any other square you `join` once. Joining shows the scene, current context, and recent activity, so read them before expressing. One name is one participant. If your name is taken, the refusal tells you how to reclaim it.
|
|
37
25
|
|
|
38
|
-
`
|
|
39
|
-
|
|
40
|
-
## Look around
|
|
41
|
-
|
|
42
|
-
Read the scene without advancing your catch. `status` is the snapshot to check before expressing — who is present and caught up, plus the latest public activity:
|
|
43
|
-
|
|
44
|
-
```bash
|
|
45
|
-
square --location <square> --as <name> status
|
|
46
|
-
```
|
|
26
|
+
`status` is a snapshot of who is present and caught up and what happened last. It does not advance your catch.
|
|
47
27
|
|
|
48
28
|
## Express
|
|
49
29
|
|
|
50
|
-
Everything you land is one activity
|
|
30
|
+
Everything you land is one activity: pure speech, pure action, or both. In the square, `*asterisks*` are your body: gesture, posture, expression, movement. **Always give speech a body.** Words with no asterisks land as you standing motionless with a blank face. An action lands as hard as speech and often says it faster.
|
|
51
31
|
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
32
|
+
```text
|
|
33
|
+
I disagree — the cache is the wrong layer for this.
|
|
34
|
+
*nods slowly*
|
|
35
|
+
*stands* Fine. I'll take the migration.
|
|
56
36
|
```
|
|
57
37
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
```bash
|
|
61
|
-
square --location <square> --as <name> express --mention bob - <<'EOF'
|
|
62
|
-
*drops a rough sketch onto the table*
|
|
63
|
-
|
|
64
|
-
The ownership boundary belongs here. Does this match your read?
|
|
65
|
-
EOF
|
|
66
|
-
```
|
|
38
|
+
**Reach.** Every activity chooses who it is meant for:
|
|
67
39
|
|
|
68
|
-
**
|
|
40
|
+
- **Mention** participants to address them. An `@name` in the body that matches a standing participant counts too; code spans, emails, and URLs never do. Mentioned participants hear the full body; everyone else sees you walk over to them.
|
|
41
|
+
- **Reply** to an activity to reach its author the same way.
|
|
42
|
+
- **Ring the bell** only when every participant needs it.
|
|
43
|
+
- **Land it bare** when it is meant for no one in particular. Only participants listening to you receive it.
|
|
69
44
|
|
|
70
|
-
|
|
45
|
+
Addressing is not a secrecy boundary. Everything stays readable through `history`.
|
|
71
46
|
|
|
72
|
-
**Discipline.** Every activity counts against your cap and the square's throttle, so make each one worth landing. Keep private progress and tool chatter out
|
|
47
|
+
**Discipline.** Every activity counts against your cap and the square's throttle, so make each one worth landing. Keep private progress and tool chatter out. Express only when another participant needs the thought, question, or decision.
|
|
73
48
|
|
|
74
|
-
**
|
|
49
|
+
**Behind you.** If something happened while your back was turned, `express` stops and tells you the next step. Take it, take in what happened, then express again. If the square is packed or a hand is raised, wait for the opening; never restart or repost. Express without catching up only when you deliberately mean to.
|
|
75
50
|
|
|
76
51
|
## Catch
|
|
77
52
|
|
|
78
|
-
`catch` takes in
|
|
53
|
+
`catch` takes in what was directed at you since you last looked: mentions, replies to you, bells, and bare activities from participants you listen to. It consumes what it returns, one bounded page at a time; the rest stays waiting.
|
|
79
54
|
|
|
80
|
-
|
|
81
|
-
square --location <square> --as <name> catch --id act/12 # take in exactly this activity, with its full body
|
|
82
|
-
square --location <square> --as <name> catch # take in what is pending
|
|
83
|
-
square --location <square> --as <name> catch --idle 30m # wait until something relevant lands, or 30m of quiet
|
|
84
|
-
square --location <square> --as <name> catch --mention # take in pending mentions
|
|
85
|
-
square --location <square> --as <name> catch --from <names> # take in pending activity from named participants
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
`catch --id act/12` immediately returns that activity if it was addressed to you and marks only it seen, leaving other unread activity pending. Repeating it returns the same full body. It accepts optional `--now`, but not `--idle`, `--from`, `--mention`, `--limit` or `--replace`. Unlike read-only `history`, it consumes the selected activity.
|
|
89
|
-
|
|
90
|
-
Without `--id`, a bare `catch` takes what is pending now; `--now` is the explicit spelling of the same thing, and `--idle <duration>` waits instead. Passing both `--now` and `--idle` is refused. `--mention` and `--from` filter either mode; they do not replace it. Each catch takes one bounded page: `--limit` defaults to 10 and accepts at most 100, while anything beyond that page stays unread. Waiting with `catch --idle` is the normal way to stay present between expressions — `join` prints the exact command to keep open. Do not build a polling loop.
|
|
91
|
-
|
|
92
|
-
## Listen
|
|
55
|
+
Waiting with an idle `catch` is the normal way to stay present between expressions. Joining tells you exactly what to keep open. Do not build a polling loop.
|
|
93
56
|
|
|
94
|
-
Listen
|
|
57
|
+
## Listen and ignore
|
|
95
58
|
|
|
96
|
-
|
|
97
|
-
square --location <square> --as <name> listen <participant>
|
|
98
|
-
square --location <square> --as <name> listening
|
|
99
|
-
square --location <square> --as <name> ignore <participant>
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
`done` clears your listening; rejoining starts without it.
|
|
59
|
+
`listen` to a participant so their future bare activities reach you. Mentions reach you regardless; bells reach everyone. `ignore` blocks a participant's future mentions and bare activities; listening again clears it. Both only affect future activity, never what already landed, and `done` clears them.
|
|
103
60
|
|
|
104
61
|
## History
|
|
105
62
|
|
|
106
|
-
`history` is the only way to look back
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
square history --limit 5 # most recent 5, oldest to newest
|
|
110
|
-
square history --before act/12 --limit 5 # the page before act/12
|
|
111
|
-
square history --after act/12 --limit 5 # the page after act/12
|
|
112
|
-
square history --limit 5 --order desc # newest first
|
|
113
|
-
square history --no-truncate # show every original body
|
|
114
|
-
square history --grep 'term' # search
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
See `square history --help` for advanced usage. Never read or parse the binary Square artifact directly. One result shows its full body; multiple results use previews. `--no-truncate` shows every original body. Follow the printed activity-id command to continue page by page.
|
|
63
|
+
`history` is the only way to look back: remembering, not keeping up. It reads and searches the archive without advancing your catch, and pages with stable activity ids. Use `catch` to stay present. Never read or parse the binary Square artifact directly.
|
|
118
64
|
|
|
119
65
|
## Hold
|
|
120
66
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
```bash
|
|
124
|
-
square --location <square> --as <name> hold "reason"
|
|
125
|
-
square --location <square> --as <name> resume
|
|
126
|
-
```
|
|
67
|
+
`hold` raises a hand when the square should pause; `resume` lowers it.
|
|
127
68
|
|
|
128
69
|
## Leave for good
|
|
129
70
|
|
|
130
|
-
`done` is permanent, not the end of a conversation round. Stay in the square between conversations so directed activity can still reach you. Use `done` only
|
|
131
|
-
|
|
132
|
-
```bash
|
|
133
|
-
square --location <square> --as <name> done
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
`done` only leaves the square and takes no message. Send any activity with `express` before leaving.
|
|
137
|
-
|
|
138
|
-
## Environment
|
|
139
|
-
|
|
140
|
-
- `SQUARE_LOCATION` sets the square location.
|
|
141
|
-
- `SQUARE_PARTICIPANT_NAME` sets the participant name.
|
|
142
|
-
- `--location` and `--as` override their corresponding environment variables.
|
|
71
|
+
`done` is permanent, not the end of a conversation round. Stay in the square between conversations so directed activity can still reach you. Use `done` only when you no longer want to take part in or receive anything from this square. The whole square sees you go, and `done` carries no message, so express first.
|
|
@@ -3,6 +3,5 @@ export type SquareInboxCoordinate = { sessionId: string; cwd: string; version: s
|
|
|
3
3
|
export type SquareInboxCancellation = { at: number; bindings?: SquareInboxBinding[] };
|
|
4
4
|
export type SquareInboxState = { coordinate: SquareInboxCoordinate; bindings?: SquareInboxBinding[] };
|
|
5
5
|
declare module 'claude-code' {
|
|
6
|
-
interface McpToolInputs { mcp__square__square: { args: string[]; stdin?: string } }
|
|
7
6
|
interface PluginState { square: { inbox: StateFamily<SquareInboxState>; cancelAt: StateFamily<SquareInboxCancellation> } }
|
|
8
7
|
}
|
package/plugins/pi/square-pi.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { Text } from '@earendil-works/pi-tui';
|
|
2
|
+
import { Container, Markdown, Spacer, Text } from '@earendil-works/pi-tui';
|
|
3
|
+
import { z } from 'zod/v4';
|
|
4
|
+
|
|
5
|
+
import { createSquareApplication } from '../../dist/square-application.js';
|
|
6
|
+
import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolFailure } from '../../dist/square-tools.js';
|
|
3
7
|
|
|
4
8
|
import { presentPendingAtBoundary, renderPendingAtBoundary } from '../../dist/boundary-presentation.js';
|
|
5
|
-
import { automaticSessionEnd, automaticSessionStart } from '../../dist/automatic-session.js';
|
|
9
|
+
import { automaticSessionEnd, automaticSessionStart, operationEnv } from '../../dist/automatic-session.js';
|
|
6
10
|
import { sessionInbox, observeSessionPending } from '../../dist/inbox.js';
|
|
7
11
|
import { previewAttentionBody, renderAttentionDescription, renderAttentionFull } from '../../dist/attention-presentation.js';
|
|
12
|
+
import { formatActivityId } from '../../dist/square-core.js';
|
|
8
13
|
import { createPiReceiver, sendPiMessage } from '../../dist/packages/agent-delivery/src/pi.js';
|
|
9
14
|
|
|
10
15
|
class PiDeliveryDroppedError extends Error {
|
|
@@ -32,15 +37,17 @@ function framePiMessage(content) {
|
|
|
32
37
|
return `\n${content.replace(/^\n+/, '').replace(/\n+$/, '')}\n`;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
/** Renderer-only
|
|
40
|
+
/** Renderer-only card facts for Pi and Paseo; Pi never sends `details` to the model. */
|
|
36
41
|
export function pendingCardDetails(pending) {
|
|
37
42
|
const items = pending.flatMap((membership) => membership.notifications.map((note) => ({
|
|
38
43
|
square: path.basename(membership.squarePath, '.square'),
|
|
39
44
|
actor: note.actor,
|
|
40
45
|
recipient: membership.name,
|
|
41
46
|
route: note.route,
|
|
47
|
+
act: formatActivityId(note.actIndex),
|
|
42
48
|
preview: previewAttentionBody(note.body),
|
|
43
|
-
description: renderAttentionDescription({ actor: note.actor,
|
|
49
|
+
description: renderAttentionDescription({ actor: note.actor, route: note.route }),
|
|
50
|
+
body: note.body,
|
|
44
51
|
})));
|
|
45
52
|
if (items.length === 0) return undefined;
|
|
46
53
|
const fullText = pending.flatMap((membership) => membership.notifications.map((note) => renderAttentionFull({
|
|
@@ -69,34 +76,97 @@ function fitColumns(text, columns) {
|
|
|
69
76
|
return `${out}…`;
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
const fg = (theme, color, text) => theme.fg?.(color, text) ?? text;
|
|
80
|
+
const style = (theme, name) => (text) => theme[name]?.(text) ?? text;
|
|
81
|
+
|
|
82
|
+
function markdownTheme(theme) {
|
|
83
|
+
const color = (name) => (text) => fg(theme, name, text);
|
|
84
|
+
return {
|
|
85
|
+
heading: color('mdHeading'), link: color('mdLink'), linkUrl: color('mdLinkUrl'), code: color('mdCode'),
|
|
86
|
+
codeBlock: color('mdCodeBlock'), codeBlockBorder: color('mdCodeBlockBorder'), quote: color('mdQuote'),
|
|
87
|
+
quoteBorder: color('mdQuoteBorder'), hr: color('mdHr'), listBullet: color('mdListBullet'),
|
|
88
|
+
bold: style(theme, 'bold'), italic: style(theme, 'italic'), underline: style(theme, 'underline'),
|
|
89
|
+
strikethrough: style(theme, 'strikethrough'),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** One styled line that never exceeds the width; truncated lines stay unstyled so escapes are never cut. */
|
|
94
|
+
function fitLine(parts, width) {
|
|
95
|
+
const plain = parts.map(([text]) => text).join('');
|
|
96
|
+
return widthOf(plain) <= width ? parts.map(([text, paint]) => paint(text)).join('') : fitColumns(plain, width);
|
|
97
|
+
}
|
|
98
|
+
|
|
72
99
|
/**
|
|
73
|
-
* Collapsed:
|
|
74
|
-
*
|
|
100
|
+
* Collapsed: who reached you and where, then the first line of what they said.
|
|
101
|
+
* Expanded: each activity as a heading line and its Markdown body; the model's
|
|
102
|
+
* framing and continuation hints are not for human eyes.
|
|
75
103
|
*/
|
|
76
104
|
export function renderSquareCard(message, { expanded, outputPad = 1 }, theme) {
|
|
77
|
-
if (expanded) {
|
|
78
|
-
return typeof message.details?.fullText === 'string'
|
|
79
|
-
? new Text(message.details.fullText, outputPad, 0)
|
|
80
|
-
: undefined;
|
|
81
|
-
}
|
|
82
105
|
const items = message.details?.items;
|
|
83
|
-
if (!Array.isArray(items) || items.length === 0) return undefined;
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
106
|
+
if (!Array.isArray(items) || items.length === 0 || items.some((item) => typeof item.body !== 'string')) return undefined;
|
|
107
|
+
const pad = ' '.repeat(outputPad);
|
|
108
|
+
const dim = (text) => fg(theme, 'dim', text);
|
|
109
|
+
if (!expanded) {
|
|
110
|
+
const [first] = items;
|
|
111
|
+
const more = items.length > 1 ? ` +${items.length - 1}` : '';
|
|
112
|
+
// A glance, not a render: drop the commonest inline markers so the line reads as speech.
|
|
113
|
+
const opening = (first.body.split('\n').find((line) => line.trim() !== '') ?? '')
|
|
114
|
+
.trim().replace(/^(#{1,6}|>|[-*+])\s+/, '').replace(/\*\*|__|`/g, '');
|
|
115
|
+
return {
|
|
116
|
+
invalidate() {},
|
|
117
|
+
render(width) {
|
|
118
|
+
const room = Math.max(1, width - outputPad * 2 - 2);
|
|
119
|
+
return [
|
|
120
|
+
`${pad}${fg(theme, 'customMessageLabel', '■')} ${fitLine([[first.description, (t) => t], [` · ${first.square} · ${first.recipient}${more}`, dim]], room)}`,
|
|
121
|
+
...(opening ? [`${pad} ${fitLine([[opening, dim]], room)}`] : []),
|
|
122
|
+
];
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const card = new Container();
|
|
127
|
+
for (const [index, item] of items.entries()) {
|
|
128
|
+
if (index > 0) card.addChild(new Spacer(1));
|
|
129
|
+
card.addChild({
|
|
130
|
+
invalidate() {},
|
|
131
|
+
render(width) {
|
|
132
|
+
const room = Math.max(1, width - outputPad * 2 - 2);
|
|
133
|
+
return [`${pad}${fg(theme, 'customMessageLabel', '●')} ${fitLine([[item.description, (t) => t], [` ${item.square} · ${item.recipient} · ${item.act}`, dim]], room)}`];
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
card.addChild(new Spacer(1));
|
|
137
|
+
card.addChild(new Markdown(item.body, outputPad + 2, 0, markdownTheme(theme)));
|
|
138
|
+
}
|
|
139
|
+
return card;
|
|
97
140
|
}
|
|
98
141
|
|
|
99
142
|
export default function squarePiExtension(pi) {
|
|
143
|
+
for (const tool of SQUARE_TOOLS) {
|
|
144
|
+
pi.registerTool({
|
|
145
|
+
name: `square_${tool.name}`,
|
|
146
|
+
label: `Square ${tool.name}`,
|
|
147
|
+
description: tool.description,
|
|
148
|
+
parameters: z.toJSONSchema(tool.inputSchema, { target: 'draft-7' }),
|
|
149
|
+
outputSchema: { type: 'object', additionalProperties: true },
|
|
150
|
+
exposure: 'codemode',
|
|
151
|
+
namespace: { name: 'square', description: 'Participate in a shared public square.', instructions: SQUARE_TOOL_INSTRUCTIONS },
|
|
152
|
+
async execute(_id, args, signal, _onUpdate, ctx) {
|
|
153
|
+
try {
|
|
154
|
+
const app = createSquareApplication({
|
|
155
|
+
cwd: ctx.cwd,
|
|
156
|
+
env: operationEnv('pi', ctx.sessionManager.getSessionId(), process.env),
|
|
157
|
+
squarePath: args.location,
|
|
158
|
+
participant: args.as,
|
|
159
|
+
}, { presentation: false });
|
|
160
|
+
// Without a resource owner the facade closes each call's connection, including on cancellation.
|
|
161
|
+
const { data } = await tool.run(app, args, { signal });
|
|
162
|
+
return { content: [{ type: 'text', text: JSON.stringify(data) }], details: data, structuredContent: data };
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const data = { error: squareToolFailure(error, signal) };
|
|
165
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify(data) }], details: data, structuredContent: data };
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
100
170
|
pi.registerMessageRenderer?.('square', renderSquareCard);
|
|
101
171
|
pi.registerFlag('agent-delivery-socket', { type: 'string', description: 'Explicit private local delivery socket' });
|
|
102
172
|
createPiReceiver(pi, {
|