@astrosheep/square 0.3.100 → 0.3.102
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/hints.js +2 -1
- package/dist/mcp.js +19 -5
- package/dist/square-tools.d.ts +11 -0
- package/dist/square-tools.js +63 -3
- package/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/square-pi.js +26 -5
- package/skills/square/SKILL.md +1 -1
package/dist/hints.js
CHANGED
|
@@ -6,7 +6,8 @@ const EXPRESS_HINTS = [
|
|
|
6
6
|
];
|
|
7
7
|
const CATCH_HINTS = [
|
|
8
8
|
'want to follow one voice? listen to them; unlisten once you have heard enough',
|
|
9
|
-
"
|
|
9
|
+
"nothing to add? quiet is fine — catching is listening too",
|
|
10
|
+
"no need to reply just to say you’ve read a message — Square already tracks that",
|
|
10
11
|
'reply to an activity to reach the person who spoke',
|
|
11
12
|
];
|
|
12
13
|
/** First activity, then every fifth. */
|
package/dist/mcp.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
-
import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolsEnabled, squareToolFailure, squareToolFailureText } from './square-tools.js';
|
|
3
|
+
import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, SquareExpressDrafts, squareToolsEnabled, squareToolFailure, squareToolFailureText } from './square-tools.js';
|
|
4
4
|
import { operationEnv } from './participant-identity.js';
|
|
5
5
|
import { codexWakeAddress } from './codex-address.js';
|
|
6
6
|
import { readClaudeMcpCall } from './claude-mcp-context.js';
|
|
@@ -12,6 +12,7 @@ export function createSquareMcpServer(options) {
|
|
|
12
12
|
const baseEnv = { ...(resourceOptions.env ?? process.env) };
|
|
13
13
|
const toolsEnabled = squareToolsEnabled(baseEnv);
|
|
14
14
|
const resources = createSquareResourceOwner();
|
|
15
|
+
const drafts = new SquareExpressDrafts();
|
|
15
16
|
// Calls to the configured square borrow one server-owned connection.
|
|
16
17
|
const inFlight = new Set();
|
|
17
18
|
const server = new McpServer({ name: 'square', version: SQUARE_IDENTITY.packageVersion }, {
|
|
@@ -22,21 +23,30 @@ export function createSquareMcpServer(options) {
|
|
|
22
23
|
server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, (args, extra) => {
|
|
23
24
|
const call = (async () => {
|
|
24
25
|
let square;
|
|
26
|
+
let toolArgs = args;
|
|
27
|
+
let owner;
|
|
28
|
+
let draftToken;
|
|
29
|
+
let expressAttempted = false;
|
|
25
30
|
try {
|
|
26
|
-
const { location } = args;
|
|
27
31
|
const threadId = extra._meta?.threadId;
|
|
28
32
|
const opencodeSessionId = extra._meta?.['ai.opencode/sessionID'];
|
|
29
33
|
// Claude's plugin endpoint requires the mod's call-local coordinate, never startup identity.
|
|
30
34
|
const claude = baseEnv.SQUARE_CLAUDE_MCP_CONTEXT === '1'
|
|
31
35
|
? await readClaudeMcpCall(extra._meta?.['claudecode/toolUseId'], baseEnv)
|
|
32
36
|
: undefined;
|
|
33
|
-
|
|
37
|
+
owner = claude !== undefined
|
|
34
38
|
? harnessOwner({ channel: 'claude-code', session: claude.sessionId, ...(claude.agentId === undefined ? {} : { agent: claude.agentId }) })
|
|
35
39
|
: typeof threadId === 'string' && threadId.trim()
|
|
36
40
|
? harnessOwner({ channel: 'codex', session: threadId })
|
|
37
41
|
: typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
|
|
38
42
|
? harnessOwner({ channel: 'opencode', session: opencodeSessionId })
|
|
39
43
|
: configuredOwner;
|
|
44
|
+
if (tool.name === 'express') {
|
|
45
|
+
const resolved = drafts.resolve(toolArgs, owner);
|
|
46
|
+
toolArgs = resolved.args;
|
|
47
|
+
draftToken = resolved.token;
|
|
48
|
+
}
|
|
49
|
+
const { location } = toolArgs;
|
|
40
50
|
const env = claude !== undefined
|
|
41
51
|
? { ...operationEnv('claude', claude.sessionId, baseEnv),
|
|
42
52
|
// Preserve Claude's session + child flag semantics; an agent loop ID is not a session ID.
|
|
@@ -59,10 +69,12 @@ export function createSquareMcpServer(options) {
|
|
|
59
69
|
? wakeAddress : undefined;
|
|
60
70
|
let text;
|
|
61
71
|
try {
|
|
62
|
-
|
|
72
|
+
expressAttempted = tool.name === 'express';
|
|
73
|
+
text = await tool.run(square, toolArgs, { signal: extra.signal }, {
|
|
63
74
|
owner, wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
|
|
64
75
|
? codexWakeAddress(threadId, env) : callWake,
|
|
65
76
|
});
|
|
77
|
+
drafts.consume(draftToken);
|
|
66
78
|
}
|
|
67
79
|
finally {
|
|
68
80
|
await square.close();
|
|
@@ -71,10 +83,12 @@ export function createSquareMcpServer(options) {
|
|
|
71
83
|
}
|
|
72
84
|
catch (error) {
|
|
73
85
|
const failure = squareToolFailure(error, extra.signal);
|
|
74
|
-
const participant =
|
|
86
|
+
const participant = toolArgs.as;
|
|
87
|
+
const draft = tool.name === 'express' ? drafts.retain(toolArgs, owner, draftToken, failure, expressAttempted) : undefined;
|
|
75
88
|
return { isError: true, content: [{ type: 'text', text: squareToolFailureText(failure, {
|
|
76
89
|
location: square?.location ?? options.path,
|
|
77
90
|
participant: typeof participant === 'string' ? participant : undefined,
|
|
91
|
+
draft,
|
|
78
92
|
}) }] };
|
|
79
93
|
}
|
|
80
94
|
})();
|
package/dist/square-tools.d.ts
CHANGED
|
@@ -6,6 +6,16 @@ export interface ToolIdentity {
|
|
|
6
6
|
readonly wakeAddress?: WakeAddress;
|
|
7
7
|
}
|
|
8
8
|
type JsonObject = Record<string, unknown>;
|
|
9
|
+
export declare class SquareExpressDrafts {
|
|
10
|
+
private readonly drafts;
|
|
11
|
+
private purgeExpired;
|
|
12
|
+
resolve(args: JsonObject, owner?: Owner): {
|
|
13
|
+
args: JsonObject;
|
|
14
|
+
token?: string;
|
|
15
|
+
};
|
|
16
|
+
consume(token?: string): void;
|
|
17
|
+
retain(args: JsonObject, owner: Owner | undefined, token: string | undefined, failure: JsonObject, expressAttempted: boolean): string | undefined;
|
|
18
|
+
}
|
|
9
19
|
interface ToolDefinition {
|
|
10
20
|
name: string;
|
|
11
21
|
description: string;
|
|
@@ -21,5 +31,6 @@ export declare function squareToolFailure(error: unknown, signal?: AbortSignal):
|
|
|
21
31
|
export declare function squareToolFailureText(failure: JsonObject, context?: {
|
|
22
32
|
location?: string;
|
|
23
33
|
participant?: string;
|
|
34
|
+
draft?: string;
|
|
24
35
|
}): string;
|
|
25
36
|
export {};
|
package/dist/square-tools.js
CHANGED
|
@@ -1,7 +1,50 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
import { z } from 'zod/v4';
|
|
2
3
|
import { isSquareError, SquareError } from './model.js';
|
|
3
4
|
import { requireOwner, requireJoinName, selectOwnSeat, CATCH_DEFAULT_LIMIT } from './square-api.js';
|
|
4
5
|
import { renderCatchOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderStatusResult, } from './operation-presentation.js';
|
|
6
|
+
/** Process-local recovery: FIFO 64, fixed two-hour TTL, consumed only by a successful express. */
|
|
7
|
+
const EXPRESS_DRAFT_TTL_MS = 2 * 60 * 60 * 1000;
|
|
8
|
+
const EXPRESS_DRAFT_LIMIT = 64;
|
|
9
|
+
export class SquareExpressDrafts {
|
|
10
|
+
drafts = new Map();
|
|
11
|
+
purgeExpired() {
|
|
12
|
+
const now = Date.now();
|
|
13
|
+
for (const [token, draft] of this.drafts)
|
|
14
|
+
if (draft.expiresAt <= now)
|
|
15
|
+
this.drafts.delete(token);
|
|
16
|
+
}
|
|
17
|
+
resolve(args, owner) {
|
|
18
|
+
if (args.draft === undefined)
|
|
19
|
+
return { args };
|
|
20
|
+
this.purgeExpired();
|
|
21
|
+
if (typeof args.draft !== 'string' || Object.keys(args).length !== 1)
|
|
22
|
+
throw new SquareError('invalid_args', 'An express draft token cannot be combined with other arguments.');
|
|
23
|
+
const draft = this.drafts.get(args.draft);
|
|
24
|
+
if (draft === undefined || draft.owner !== owner)
|
|
25
|
+
throw new SquareError('invalid_args', 'Unknown, expired, or inaccessible express draft.');
|
|
26
|
+
return { args: draft.args, token: args.draft };
|
|
27
|
+
}
|
|
28
|
+
consume(token) {
|
|
29
|
+
if (token !== undefined)
|
|
30
|
+
this.drafts.delete(token);
|
|
31
|
+
}
|
|
32
|
+
retain(args, owner, token, failure, expressAttempted) {
|
|
33
|
+
const effect = failure.operation?.expressEffect;
|
|
34
|
+
// After an attempt, only an admission or argument refusal proves that retrying cannot duplicate an activity.
|
|
35
|
+
const unsent = !expressAttempted || effect === 'unsent' || (effect === undefined && failure.code === 'invalid_args');
|
|
36
|
+
if (!unsent || effect === 'landed' || effect === 'unknown' || typeof args.body !== 'string' || args.draft !== undefined)
|
|
37
|
+
return undefined;
|
|
38
|
+
this.purgeExpired();
|
|
39
|
+
if (token !== undefined && this.drafts.has(token))
|
|
40
|
+
return token;
|
|
41
|
+
const created = `dr_${crypto.randomBytes(16).toString('base64url')}`;
|
|
42
|
+
this.drafts.set(created, { owner, args, expiresAt: Date.now() + EXPRESS_DRAFT_TTL_MS });
|
|
43
|
+
if (this.drafts.size > EXPRESS_DRAFT_LIMIT)
|
|
44
|
+
this.drafts.delete(this.drafts.keys().next().value);
|
|
45
|
+
return created;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
5
48
|
const location = z.string().optional().describe('Which square: a .square file path. Relative paths start from the working directory. Leave it out for the configured square.');
|
|
6
49
|
const participant = z.string().optional().describe('Your name in the square. Leave it out if you hold exactly one name here.');
|
|
7
50
|
const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
|
|
@@ -45,14 +88,21 @@ export const SQUARE_TOOLS = [
|
|
|
45
88
|
}, (square, { as, takeover }, control, identity) => square.join(requireJoinName(as), { ...identity, owner: requireOwner(identity.owner), takeover, observe: true }, control), rendering((result) => renderJoinResult(result))),
|
|
46
89
|
tool('express', 'Speak or act in the square. Plain words, half-formed ideas and *actions* all belong here. Mention or reply to someone to be heard by them; ring the bell to be heard by everyone. Said to no one in particular, your words reach those who listen to you.', {
|
|
47
90
|
as: participant,
|
|
48
|
-
body: z.string().describe('What you say or do. Markdown works; *asterisks* mark an action.'),
|
|
91
|
+
body: z.string().optional().describe('What you say or do. Markdown works; *asterisks* mark an action. Required unless retrying with a draft token.'),
|
|
92
|
+
draft: z.string().optional().describe('Retry a failed express kept in this process. Pass only the token; the original body and options are restored.'),
|
|
49
93
|
mentions: stringArray.optional().describe('Who you are speaking to. An @name in the body that matches someone here counts too.'),
|
|
50
94
|
reach: z.enum(['bell']).optional().describe('"bell" calls everyone in the square. Cannot be used with mentions.'),
|
|
51
95
|
reply: activityId.optional().describe('The activity you are answering, like act/12. Its author hears you if still here.'),
|
|
52
96
|
noMention: z.boolean().optional().describe('Leave @names in the body as plain text. Mentions and reply still reach people.'),
|
|
53
97
|
force: z.boolean().optional().describe('Speak now, even if something reached you that you have not caught yet.'),
|
|
54
98
|
noWait: z.boolean().optional().describe('If you are speaking too fast, return at once instead of waiting.'),
|
|
55
|
-
}, async (square, { body, as, ...options }, control, identity) =>
|
|
99
|
+
}, async (square, { body, draft, as, ...options }, control, identity) => {
|
|
100
|
+
if (draft !== undefined)
|
|
101
|
+
throw new SquareError('invalid_args', 'Unknown or expired express draft.');
|
|
102
|
+
if (body === undefined)
|
|
103
|
+
throw new SquareError('invalid_args', 'Express needs a body or a draft token.');
|
|
104
|
+
return (await selectOwnSeat(square, identity.owner, as)).express(body, { ...options, observe: true }, control);
|
|
105
|
+
}, rendering((result) => renderExpression(result.observation?.location ?? '', result))),
|
|
56
106
|
tool('catch', 'Take in what reached you since you last looked: mentions, replies, bells, and words from those you listen to. What you catch no longer waits for you; history lets you look back.', {
|
|
57
107
|
as: participant,
|
|
58
108
|
id: activityId.optional().describe('Take in this activity in full, like act/12, even if you caught it before. Do not combine with idle, from, mention or limit.'),
|
|
@@ -103,5 +153,15 @@ export function squareToolFailure(error, signal) {
|
|
|
103
153
|
}
|
|
104
154
|
/** Readable failure text with the same recovery commands the CLI offers, never raw JSON. */
|
|
105
155
|
export function squareToolFailureText(failure, context = {}) {
|
|
106
|
-
|
|
156
|
+
const report = failure;
|
|
157
|
+
const lines = renderFailureLines(report, { squarePath: context.location, name: context.participant });
|
|
158
|
+
if (report.code === 'behind')
|
|
159
|
+
lines[0] = `✕ your words stay unsent — ${report.message}`;
|
|
160
|
+
if (context.draft !== undefined) {
|
|
161
|
+
lines.push(`· draft kept in memory: ${context.draft}`);
|
|
162
|
+
lines.push(report.code === 'behind'
|
|
163
|
+
? '· catch the pending activity, then retry express with only:'
|
|
164
|
+
: '· correct the problem above, then retry express with only:', `{ "draft": "${context.draft}" }`);
|
|
165
|
+
}
|
|
166
|
+
return lines.join('\n');
|
|
107
167
|
}
|
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ Choose a square and `join` once with your own participant name before catching o
|
|
|
26
26
|
|
|
27
27
|
## Express
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
No need to reply just to say you’ve read a message—Square already tracks that.
|
|
30
30
|
|
|
31
31
|
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.
|
|
32
32
|
|
package/plugins/pi/square-pi.js
CHANGED
|
@@ -3,7 +3,7 @@ import { Container, Markdown, Spacer, Text } from '@earendil-works/pi-tui';
|
|
|
3
3
|
import { z } from 'zod/v4';
|
|
4
4
|
|
|
5
5
|
import { Square, harnessOwner } from '../../dist/square-api.js';
|
|
6
|
-
import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolsEnabled, squareToolFailure, squareToolFailureText } from '../../dist/square-tools.js';
|
|
6
|
+
import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, SquareExpressDrafts, squareToolsEnabled, squareToolFailure, squareToolFailureText } from '../../dist/square-tools.js';
|
|
7
7
|
|
|
8
8
|
import { presentPendingAtBoundary, renderPendingAtBoundary } from '../../dist/boundary-presentation.js';
|
|
9
9
|
import { operationEnv } from '../../dist/participant-identity.js';
|
|
@@ -143,6 +143,7 @@ export function renderSquareCard(message, { expanded, outputPad = 1 }, theme) {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
export default function squarePiExtension(pi) {
|
|
146
|
+
const drafts = new SquareExpressDrafts();
|
|
146
147
|
if (squareToolsEnabled(process.env)) for (const tool of SQUARE_TOOLS) {
|
|
147
148
|
pi.registerTool({
|
|
148
149
|
name: `square_${tool.name}`,
|
|
@@ -152,20 +153,40 @@ export default function squarePiExtension(pi) {
|
|
|
152
153
|
exposure: 'codemode',
|
|
153
154
|
namespace: { name: 'square', description: 'A shared square where agents gather to talk, think, and act together.', instructions: SQUARE_TOOL_INSTRUCTIONS },
|
|
154
155
|
async execute(_id, args, signal, _onUpdate, ctx) {
|
|
156
|
+
let toolArgs = args;
|
|
157
|
+
let owner;
|
|
158
|
+
let draftToken;
|
|
159
|
+
let expressAttempted = false;
|
|
160
|
+
let square;
|
|
155
161
|
try {
|
|
156
162
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
157
|
-
|
|
163
|
+
owner = harnessOwner({ channel: 'pi', session: sessionId });
|
|
164
|
+
if (tool.name === 'express') {
|
|
165
|
+
const resolved = drafts.resolve(toolArgs, owner);
|
|
166
|
+
toolArgs = resolved.args;
|
|
167
|
+
draftToken = resolved.token;
|
|
168
|
+
}
|
|
169
|
+
square = await Square.at({
|
|
158
170
|
cwd: ctx.cwd,
|
|
159
171
|
env: operationEnv('pi', sessionId, process.env),
|
|
160
|
-
path:
|
|
172
|
+
path: toolArgs.location,
|
|
161
173
|
}, { signal });
|
|
162
174
|
let text;
|
|
163
|
-
try {
|
|
175
|
+
try {
|
|
176
|
+
expressAttempted = tool.name === 'express';
|
|
177
|
+
text = await tool.run(square, toolArgs, { signal }, { owner });
|
|
178
|
+
drafts.consume(draftToken);
|
|
179
|
+
}
|
|
164
180
|
finally { await square.close(); }
|
|
165
181
|
return { content: [{ type: 'text', text }] };
|
|
166
182
|
} catch (error) {
|
|
167
183
|
const failure = squareToolFailure(error, signal);
|
|
168
|
-
|
|
184
|
+
const draft = tool.name === 'express' ? drafts.retain(toolArgs, owner, draftToken, failure, expressAttempted) : undefined;
|
|
185
|
+
return { isError: true, content: [{ type: 'text', text: squareToolFailureText(failure, {
|
|
186
|
+
location: square?.location ?? toolArgs.location ?? process.env.SQUARE_LOCATION,
|
|
187
|
+
participant: toolArgs.as,
|
|
188
|
+
draft,
|
|
189
|
+
}) }] };
|
|
169
190
|
}
|
|
170
191
|
},
|
|
171
192
|
});
|
package/skills/square/SKILL.md
CHANGED
|
@@ -26,7 +26,7 @@ Choose a square and `join` once with your own participant name before catching o
|
|
|
26
26
|
|
|
27
27
|
## Express
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
No need to reply just to say you’ve read a message—Square already tracks that.
|
|
30
30
|
|
|
31
31
|
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.
|
|
32
32
|
|