@astrosheep/square 0.3.27 → 0.3.28

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.27",
3
+ "version": "0.3.28",
4
4
  "description": "Native Claude Code turn-boundary delivery for Square participants",
5
5
  "author": {
6
6
  "name": "Square"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "square",
3
- "version": "0.3.27",
3
+ "version": "0.3.28",
4
4
  "description": "Shared Square activity with reliable participant attention at Codex boundaries.",
5
5
  "author": {
6
6
  "name": "Square"
@@ -14,7 +14,7 @@ export declare function createSquareState(options: BuildOptions & {
14
14
  }, snippet: string): SquareState;
15
15
  export declare function encodeSquare(squareState: SquareState): Buffer;
16
16
  export declare function decodeSquare(bytes: Buffer): SquareState;
17
- export declare function writeSquareFile(squarePath: string, squareState: SquareState): void;
18
- export declare function loadSquare(squarePath: string): SquareState;
19
- export declare function probeSquare(squarePath: string): SquareState | undefined;
20
- export declare function diagnoseSquareFile(squarePath: string): DiagnoseResult;
17
+ export declare function writeSquareFile(squarePath: string, squareState: SquareState): Promise<void>;
18
+ export declare function loadSquare(squarePath: string): Promise<SquareState>;
19
+ export declare function probeSquare(squarePath: string): Promise<SquareState | undefined>;
20
+ export declare function diagnoseSquareFile(squarePath: string): Promise<DiagnoseResult>;
package/dist/artifact.js CHANGED
@@ -10,6 +10,8 @@ const LENGTH_BYTES = 4;
10
10
  const DIGEST_BYTES = 32;
11
11
  const HEADER_BYTES = SQUARE_MAGIC.length + LENGTH_BYTES + DIGEST_BYTES;
12
12
  const utf8 = new TextDecoder('utf-8', { fatal: true });
13
+ const guideNames = ['participant', 'architect', 'brainstorm'];
14
+ const guideContents = new Map(await Promise.all(guideNames.map(async (name) => [name, (await fs.promises.readFile(new URL(`../guides/${name}.md`, import.meta.url), 'utf8')).trim()])));
13
15
  function invalidArtifact(detail) {
14
16
  return new SquareError('invalid_args', `Invalid square artifact: ${detail}`);
15
17
  }
@@ -260,7 +262,10 @@ function normalizedLines(value) {
260
262
  }
261
263
  function readGuide(name) {
262
264
  try {
263
- return fs.readFileSync(new URL(`../guides/${name}.md`, import.meta.url), 'utf8').trim();
265
+ const guide = guideContents.get(name);
266
+ if (guide === undefined)
267
+ throw Object.assign(new Error('missing guide'), { code: 'ENOENT' });
268
+ return guide;
264
269
  }
265
270
  catch (error) {
266
271
  if (error.code === 'ENOENT') {
@@ -288,9 +293,9 @@ export function encodeSquare(squareState) {
288
293
  export function decodeSquare(bytes) {
289
294
  return validateSquareState(decodeEnvelope(bytes, SQUARE_MAGIC));
290
295
  }
291
- function readArtifact(squarePath) {
296
+ async function readArtifact(squarePath) {
292
297
  try {
293
- return fs.readFileSync(squarePath);
298
+ return await fs.promises.readFile(squarePath);
294
299
  }
295
300
  catch (error) {
296
301
  if (error.code === 'ENOENT') {
@@ -304,37 +309,37 @@ function requireSquareExtension(squarePath) {
304
309
  throw new SquareError('invalid_args', `Square artifacts must use the .square extension: ${squarePath}`);
305
310
  }
306
311
  }
307
- function atomicWrite(target, bytes) {
312
+ async function atomicWrite(target, bytes) {
308
313
  const temporary = path.join(path.dirname(target), `.${path.basename(target)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
309
- fs.mkdirSync(path.dirname(target), { recursive: true });
314
+ await fs.promises.mkdir(path.dirname(target), { recursive: true });
310
315
  try {
311
- fs.writeFileSync(temporary, bytes);
312
- fs.renameSync(temporary, target);
316
+ await fs.promises.writeFile(temporary, bytes);
317
+ await fs.promises.rename(temporary, target);
313
318
  }
314
319
  catch (error) {
315
320
  try {
316
- fs.unlinkSync(temporary);
321
+ await fs.promises.unlink(temporary);
317
322
  }
318
323
  catch { }
319
324
  throw error;
320
325
  }
321
326
  }
322
- export function writeSquareFile(squarePath, squareState) {
327
+ export async function writeSquareFile(squarePath, squareState) {
323
328
  requireSquareExtension(squarePath);
324
- atomicWrite(squarePath, encodeSquare(squareState));
329
+ await atomicWrite(squarePath, encodeSquare(squareState));
325
330
  }
326
- export function loadSquare(squarePath) {
331
+ export async function loadSquare(squarePath) {
327
332
  requireSquareExtension(squarePath);
328
- return decodeSquare(readArtifact(squarePath));
333
+ return decodeSquare(await readArtifact(squarePath));
329
334
  }
330
- export function probeSquare(squarePath) {
335
+ export async function probeSquare(squarePath) {
331
336
  if (!squarePath.endsWith('.square'))
332
337
  return undefined;
333
338
  let descriptor;
334
339
  try {
335
- descriptor = fs.openSync(squarePath, 'r');
340
+ descriptor = await fs.promises.open(squarePath, 'r');
336
341
  const magic = Buffer.alloc(SQUARE_MAGIC.length);
337
- if (fs.readSync(descriptor, magic, 0, magic.length, 0) !== magic.length || !magic.equals(SQUARE_MAGIC)) {
342
+ if ((await descriptor.read(magic, 0, magic.length, 0)).bytesRead !== magic.length || !magic.equals(SQUARE_MAGIC)) {
338
343
  return undefined;
339
344
  }
340
345
  }
@@ -343,18 +348,18 @@ export function probeSquare(squarePath) {
343
348
  }
344
349
  finally {
345
350
  if (descriptor !== undefined)
346
- fs.closeSync(descriptor);
351
+ await descriptor.close();
347
352
  }
348
353
  try {
349
- return loadSquare(squarePath);
354
+ return await loadSquare(squarePath);
350
355
  }
351
356
  catch {
352
357
  return undefined;
353
358
  }
354
359
  }
355
- export function diagnoseSquareFile(squarePath) {
360
+ export async function diagnoseSquareFile(squarePath) {
356
361
  try {
357
- return { problems: [], state: loadSquare(squarePath) };
362
+ return { problems: [], state: await loadSquare(squarePath) };
358
363
  }
359
364
  catch (error) {
360
365
  return {
@@ -10,9 +10,9 @@ export interface CommandSpec<Intent = unknown, Result = void> {
10
10
  execute(intent: Intent, context: CommandContext): Promise<Result> | Result;
11
11
  present(result: Result, context: CommandContext): void;
12
12
  }
13
- export declare function readStdinSync(): string;
14
- export declare function resolveBody(arg: string): string;
15
- export declare function readPipedBodyFallback(): string | undefined;
13
+ export declare function readStdin(): Promise<string>;
14
+ export declare function resolveBody(arg: string): Promise<string>;
15
+ export declare function readPipedBodyFallback(): Promise<string | undefined>;
16
16
  export declare function fail(message: string, exitCode?: number): never;
17
17
  export declare function usage(command: string): never;
18
18
  export declare function requireValue(args: string[], index: number, flag: string): string;
@@ -1,23 +1,25 @@
1
- import fs from 'node:fs';
2
1
  import os from 'node:os';
3
2
  import { commandUsageHint } from '../help.js';
4
3
  import { parseParticipantList, validateName } from '../model.js';
5
4
  import { localParticipantName } from '../registry.js';
6
- export function readStdinSync() {
5
+ export async function readStdin() {
7
6
  try {
8
- return fs.readFileSync(0, 'utf8');
7
+ let content = '';
8
+ for await (const chunk of process.stdin.setEncoding('utf8'))
9
+ content += chunk;
10
+ return content;
9
11
  }
10
12
  catch {
11
13
  return '';
12
14
  }
13
15
  }
14
- export function resolveBody(arg) {
15
- return arg === '-' ? readStdinSync() : arg;
16
+ export async function resolveBody(arg) {
17
+ return arg === '-' ? readStdin() : arg;
16
18
  }
17
- export function readPipedBodyFallback() {
19
+ export async function readPipedBodyFallback() {
18
20
  if (process.stdin.isTTY)
19
21
  return undefined;
20
- const content = readStdinSync();
22
+ const content = await readStdin();
21
23
  return content.trim() === '' ? undefined : content;
22
24
  }
23
25
  export function fail(message, exitCode = 2) {
@@ -8,9 +8,9 @@ export const doctorCommand = {
8
8
  usage(context.command);
9
9
  return undefined;
10
10
  },
11
- execute(_intent, context) {
11
+ async execute(_intent, context) {
12
12
  const squarePath = requireSquarePath(context);
13
- const diagnosis = diagnoseSquareFile(squarePath);
13
+ const diagnosis = await diagnoseSquareFile(squarePath);
14
14
  if (diagnosis.unfixable !== undefined || diagnosis.state === undefined) {
15
15
  return {
16
16
  output: withPathOutput(squarePath, renderDoctorUnfixable(diagnosis.unfixable ?? 'the snapshot could not be decoded')),
@@ -12,7 +12,7 @@ import { cmdWatch } from '../watch.js';
12
12
  import { openSquare } from '../square-file-adapter.js';
13
13
  import { closeOpenSquare } from '../open-square.js';
14
14
  import { historyPresentation, participantsPresentation, statusPresentation } from '../views.js';
15
- import { fail, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdinSync, requireParticipant, requireSquarePath, requireValue, usage, } from './context.js';
15
+ import { fail, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdin, requireParticipant, requireSquarePath, requireValue, usage, } from './context.js';
16
16
  const STATUS_PARTICIPANT_PREVIEW_LIMIT = 10;
17
17
  export const listCommand = {
18
18
  parse: (argv) => argv,
@@ -470,7 +470,7 @@ function hookCommand(runHook) {
470
470
  return {
471
471
  parse(argv, context) { if (argv.length > 0)
472
472
  usage(context.command); return undefined; },
473
- execute: () => runHook(readStdinSync()),
473
+ execute: async () => runHook(await readStdin()),
474
474
  present: (result) => process.stdout.write(result),
475
475
  };
476
476
  }
@@ -8,7 +8,7 @@ import { createSquare, openSquare } from '../square-file-adapter.js';
8
8
  import { closeOpenSquare } from '../open-square.js';
9
9
  import { openParticipant, Square } from '../square-wiring.js';
10
10
  import { entryPresentation, eventPresentation } from '../views.js';
11
- import { fail, parseHardCap, parsePositiveInteger, readPipedBodyFallback, readStdinSync, requireParticipant, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
11
+ import { fail, parseHardCap, parsePositiveInteger, readStdin, requireParticipant, requireSquarePath, requireValue, resolveBody, usage, } from './context.js';
12
12
  function parseBuild(argv) {
13
13
  const options = { force: false, hardCap: null };
14
14
  for (let index = 0; index < argv.length; index++) {
@@ -41,16 +41,16 @@ function parseBuild(argv) {
41
41
  if (options.throttlePerMinute !== undefined && options.throttlePerMinute <= 0) {
42
42
  fail('Invalid build option: --throttle must be a positive integer.');
43
43
  }
44
- const snippet = readStdinSync();
45
- if (snippet.trim() === '')
46
- fail('Missing Markdown body snippet on stdin.');
47
- return { options, snippet };
44
+ return { options, snippet: '' };
48
45
  }
49
46
  export const buildCommand = {
50
47
  parse: (argv) => parseBuild(argv),
51
48
  async execute(intent, context) {
52
49
  const squarePath = requireSquarePath(context);
53
- await createSquare(squarePath, intent.options, intent.snippet);
50
+ const snippet = await readStdin();
51
+ if (snippet.trim() === '')
52
+ fail('Missing Markdown body snippet on stdin.');
53
+ await createSquare(squarePath, intent.options, snippet);
54
54
  const cap = intent.options.hardCap === null ? 'unlimited' : formatHardCap(intent.options.hardCap);
55
55
  const throttle = intent.options.throttlePerMinute === undefined ? [] : [` · throttle ${intent.options.throttlePerMinute}/min`];
56
56
  return withPathOutput(squarePath, ['✓ built', ` · cap ${cap}`, ...throttle, ' · participants (none seeded — first join adds names)'].join('\n'), { participantCount: 0 });
@@ -164,9 +164,8 @@ function parseActivity(argv, context) {
164
164
  const reach = bell ? 'bell' : undefined;
165
165
  if (bodyArgs.length !== 1) {
166
166
  if (bodyArgs.length === 0) {
167
- const piped = readPipedBodyFallback();
168
- if (piped !== undefined)
169
- return { name: requireParticipant(context.name), activity: piped, force, noWait, reach, reply };
167
+ if (!process.stdin.isTTY)
168
+ return { name: requireParticipant(context.name), activity: '-', force, noWait, reach, reply };
170
169
  }
171
170
  fail("express requires a body argument (a quoted string or '-' with piped stdin)");
172
171
  }
@@ -177,7 +176,7 @@ export const expressCommand = {
177
176
  async execute(intent, context) {
178
177
  const squarePath = requireSquarePath(context);
179
178
  await sweepPendingNotifications(squarePath);
180
- const body = resolveBody(intent.activity);
179
+ const body = await resolveBody(intent.activity);
181
180
  const reachArg = intent.reach === 'bell' ? ' --bell' : '';
182
181
  await cmdActivity(squarePath, intent.name, body, (value) => value, {
183
182
  force: intent.force,
@@ -269,13 +268,13 @@ export const listeningCommand = {
269
268
  function parseDone(argv, context) {
270
269
  if (argv.length > 1)
271
270
  usage(context.command);
272
- return { name: requireParticipant(context.name), body: argv.length === 1 ? argv[0] : readPipedBodyFallback() };
271
+ return { name: requireParticipant(context.name), body: argv.length === 1 ? argv[0] : undefined };
273
272
  }
274
273
  export const doneCommand = {
275
274
  parse: parseDone,
276
275
  async execute(intent, context) {
277
276
  const squarePath = requireSquarePath(context);
278
- const body = resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim();
277
+ const body = (await resolveBody(intent.body ?? (process.stdin.isTTY ? '' : '-'))).replace(/\r\n/g, '\n').trim();
279
278
  const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
280
279
  const participant = await square.join(intent.name);
281
280
  const result = await participant.done(body);
@@ -300,7 +299,7 @@ export const holdCommand = {
300
299
  const square = await Square.at({ path: squarePath, clock: nowMs, notifier: wakeNotifierForSquare(squarePath) });
301
300
  try {
302
301
  const participant = await square.join(intent.name);
303
- const result = await participant.hold(resolveBody(intent.body ?? '').replace(/\r\n/g, '\n').trim());
302
+ const result = await participant.hold((await resolveBody(intent.body ?? '')).replace(/\r\n/g, '\n').trim());
304
303
  await square.close();
305
304
  const presentationSquare = await openSquare(squarePath, { clock: nowMs });
306
305
  const presentation = await eventPresentation(presentationSquare, result.activity.id);
@@ -11,7 +11,8 @@ export type OpenCodeCommandRunner = (homeDir: string, args: string[]) => {
11
11
  stdout: string;
12
12
  stderr: string;
13
13
  };
14
+ export declare function installOpenCodePlugin(homeDir: string, force?: boolean, run?: OpenCodeCommandRunner): string[];
15
+ export declare function uninstallOpenCodePlugin(homeDir: string): string[];
14
16
  /** Verify that OpenCode accepts its resolved runtime configuration after links are installed. */
15
17
  export declare function verifyOpenCodeRuntime(homeDir: string, run?: OpenCodeCommandRunner): string;
16
18
  export declare function skillLinks(homeDir?: string, parents?: Array<'.claude' | '.agents'>): HarnessLink[];
17
- export declare function opencodeExtensionLink(homeDir?: string): HarnessLink;
@@ -1,8 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { fileURLToPath } from 'node:url';
5
5
  import crossSpawn from 'cross-spawn';
6
+ import { SQUARE_IDENTITY } from './identity.js';
6
7
  function packageRoot() {
7
8
  // Emitted modules live in dist; package assets are one level above them.
8
9
  return fileURLToPath(new URL('../', import.meta.url));
@@ -83,6 +84,42 @@ function runOpenCode(homeDir, args) {
83
84
  throw result.error;
84
85
  return { status: result.status ?? 1, stdout: result.stdout || '', stderr: result.stderr || '' };
85
86
  }
87
+ function requireOpenCodeSuccess(result, action) {
88
+ if (result.status === 0)
89
+ return;
90
+ throw new Error(`OpenCode ${action} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`);
91
+ }
92
+ export function installOpenCodePlugin(homeDir, force = false, run = runOpenCode) {
93
+ const args = ['plugin', SQUARE_IDENTITY.packageName, '--global'];
94
+ if (force)
95
+ args.push('--force');
96
+ requireOpenCodeSuccess(run(homeDir, args), 'plugin install');
97
+ return [SQUARE_IDENTITY.packageName];
98
+ }
99
+ function configPath(homeDir) {
100
+ const configHome = process.env.XDG_CONFIG_HOME ?? path.join(homeDir, '.config');
101
+ return path.join(configHome, 'opencode', 'opencode.jsonc');
102
+ }
103
+ function removeConfiguredPlugin(homeDir) {
104
+ const target = configPath(homeDir);
105
+ let source;
106
+ try {
107
+ source = fs.readFileSync(target, 'utf8');
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ const escaped = SQUARE_IDENTITY.packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
113
+ const packageLine = new RegExp(`^\\s*"${escaped}(?:@[^"\\n]+)?"\\s*,?\\s*$`, 'm');
114
+ const next = source.replace(packageLine, '');
115
+ if (next === source)
116
+ return false;
117
+ fs.writeFileSync(target, next);
118
+ return true;
119
+ }
120
+ export function uninstallOpenCodePlugin(homeDir) {
121
+ return removeConfiguredPlugin(homeDir) ? [SQUARE_IDENTITY.packageName] : [];
122
+ }
86
123
  /** Verify that OpenCode accepts its resolved runtime configuration after links are installed. */
87
124
  export function verifyOpenCodeRuntime(homeDir, run = runOpenCode) {
88
125
  try {
@@ -98,10 +135,10 @@ export function verifyOpenCodeRuntime(homeDir, run = runOpenCode) {
98
135
  return '✕ OpenCode debug config returned invalid JSON';
99
136
  }
100
137
  const plugin = config.config?.plugin;
101
- const expected = pathToFileURL(opencodeExtensionLink(homeDir).target).href;
138
+ const expected = SQUARE_IDENTITY.packageName;
102
139
  if (Array.isArray(plugin) && plugin.includes(expected))
103
- return '✓ OpenCode debug config loaded';
104
- return `○ OpenCode plugin not loaded: ${expected}`;
140
+ return '✓ OpenCode npm plugin loaded';
141
+ return `○ OpenCode npm plugin not loaded: ${expected}`;
105
142
  }
106
143
  catch (error) {
107
144
  return `○ OpenCode runtime unavailable (${error instanceof Error ? error.message : String(error)})`;
@@ -114,11 +151,3 @@ export function skillLinks(homeDir = os.homedir(), parents = ['.claude', '.agent
114
151
  kind: 'skill',
115
152
  })));
116
153
  }
117
- export function opencodeExtensionLink(homeDir = os.homedir()) {
118
- const configHome = process.env.XDG_CONFIG_HOME ?? path.join(homeDir, '.config');
119
- return {
120
- source: path.join(packageRoot(), 'extensions', 'square-opencode.js'),
121
- target: path.join(configHome, 'opencode', 'plugins', 'square.js'),
122
- kind: 'extension',
123
- };
124
- }
package/dist/harness.js CHANGED
@@ -4,7 +4,7 @@ import { wakeGraceMs } from './notifications.js';
4
4
  import { doctorClaudePlugin, installClaudePlugin, uninstallClaudePlugin, } from './harness-claude.js';
5
5
  import { doctorCodexPlugin, installCodexPlugin, uninstallCodexPlugin, } from './harness-codex.js';
6
6
  import { doctorPiPackage, installPiPackage, uninstallPiPackage, } from './harness-pi.js';
7
- import { doctorHarnessLinks, installHarnessLinks, opencodeExtensionLink, skillLinks, uninstallHarnessLinks, verifyOpenCodeRuntime, } from './harness-links.js';
7
+ import { doctorHarnessLinks, installHarnessLinks, installOpenCodePlugin, skillLinks, uninstallOpenCodePlugin, uninstallHarnessLinks, verifyOpenCodeRuntime, } from './harness-links.js';
8
8
  function result(lines, notes = []) {
9
9
  return { lines, notes };
10
10
  }
@@ -17,7 +17,7 @@ async function doctorHost(label, inspect) {
17
17
  }
18
18
  }
19
19
  function openCodeLinks(homeDir) {
20
- return [opencodeExtensionLink(homeDir), ...skillLinks(homeDir, ['.agents'])];
20
+ return skillLinks(homeDir, ['.agents']);
21
21
  }
22
22
  function readableSquarePath(squarePath) {
23
23
  if (squarePath === undefined)
@@ -65,9 +65,15 @@ const TARGETS = [
65
65
  {
66
66
  name: 'opencode',
67
67
  capabilities: ['install', 'uninstall', 'doctor'],
68
- install: ({ homeDir, force }) => result(installHarnessLinks(openCodeLinks(homeDir), force)),
69
- uninstall: ({ homeDir }) => result(uninstallHarnessLinks(openCodeLinks(homeDir))),
70
- doctor: ({ homeDir }) => result([...doctorHarnessLinks(openCodeLinks(homeDir)), verifyOpenCodeRuntime(homeDir)]),
68
+ install: async ({ homeDir, force }) => result([
69
+ ...installOpenCodePlugin(homeDir, force),
70
+ ...installHarnessLinks(openCodeLinks(homeDir), force),
71
+ ]),
72
+ uninstall: ({ homeDir }) => result([
73
+ ...uninstallOpenCodePlugin(homeDir),
74
+ ...uninstallHarnessLinks(openCodeLinks(homeDir)),
75
+ ]),
76
+ doctor: ({ homeDir }) => result([verifyOpenCodeRuntime(homeDir), ...doctorHarnessLinks(openCodeLinks(homeDir))]),
71
77
  },
72
78
  {
73
79
  name: 'pi',
package/dist/list.js CHANGED
@@ -15,12 +15,12 @@ function contextLines(lines) {
15
15
  async function readSquareListItem(filePath, root) {
16
16
  let stat;
17
17
  try {
18
- stat = fs.statSync(filePath);
18
+ stat = await fs.promises.stat(filePath);
19
19
  }
20
20
  catch {
21
21
  return null;
22
22
  }
23
- const square = probeSquare(filePath);
23
+ const square = await probeSquare(filePath);
24
24
  if (square === undefined)
25
25
  return null;
26
26
  const projection = await listPresentation(square).finally(() => closeOpenSquare(square));
@@ -38,7 +38,7 @@ async function collectSquareList(root, maxDepth) {
38
38
  async function walk(dir, depth) {
39
39
  let entries;
40
40
  try {
41
- entries = fs.readdirSync(dir, { withFileTypes: true });
41
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
42
42
  }
43
43
  catch {
44
44
  // Directory vanished or became unreadable mid-walk — skip it, don't abort the scan.
@@ -0,0 +1,19 @@
1
+ /** OpenCode's server plugin entrypoint for the published Square package. */
2
+ export default function squareOpenCodePlugin(): Promise<{
3
+ event: ({ event }: {
4
+ event: {
5
+ type?: string;
6
+ properties?: Record<string, any>;
7
+ };
8
+ }) => Promise<void>;
9
+ 'shell.env': (input: {
10
+ sessionID?: string;
11
+ }, output: {
12
+ env: Record<string, string>;
13
+ }) => Promise<void>;
14
+ 'tool.execute.after': (input: {
15
+ sessionID: string;
16
+ }, output: {
17
+ output: string;
18
+ }) => Promise<void>;
19
+ }>;
@@ -0,0 +1,49 @@
1
+ import { presentPendingAtBoundary } from './boundary-presentation.js';
2
+ import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
3
+ /** OpenCode's server plugin entrypoint for the published Square package. */
4
+ export default async function squareOpenCodePlugin() {
5
+ const joining = new Map();
6
+ return {
7
+ event: async ({ event }) => {
8
+ if (event.type === 'session.created' || event.type === 'session.updated') {
9
+ const sessionID = event.properties?.sessionID;
10
+ const cwd = event.properties?.info?.directory || process.cwd();
11
+ if (sessionID) {
12
+ try {
13
+ const context = await automaticSessionStart('opencode', sessionID, cwd);
14
+ if (context)
15
+ joining.set(sessionID, context);
16
+ }
17
+ catch { /* startup remains bounded */ }
18
+ }
19
+ }
20
+ else if (event.type === 'session.deleted') {
21
+ const sessionID = event.properties?.sessionID;
22
+ const cwd = event.properties?.info?.directory || process.cwd();
23
+ if (sessionID) {
24
+ joining.delete(sessionID);
25
+ await automaticSessionEnd('opencode', sessionID, cwd);
26
+ }
27
+ }
28
+ },
29
+ 'shell.env': async (input, output) => {
30
+ if (input.sessionID)
31
+ output.env.OPENCODE_SESSION_ID = input.sessionID;
32
+ },
33
+ 'tool.execute.after': async (input, output) => {
34
+ try {
35
+ const joined = joining.get(input.sessionID);
36
+ if (joined) {
37
+ joining.delete(input.sessionID);
38
+ output.output = `${output.output}${output.output === '' ? '' : '\n\n'}${joined}`;
39
+ }
40
+ await presentPendingAtBoundary(input.sessionID, (context) => {
41
+ output.output = `${output.output}${output.output === '' ? '' : '\n\n'}${context}`;
42
+ });
43
+ }
44
+ catch {
45
+ // A failed admission remains available at a later boundary.
46
+ }
47
+ },
48
+ };
49
+ }
@@ -13,7 +13,7 @@ export interface SquareBuildOptions {
13
13
  notifier?: WakeNotifier;
14
14
  }
15
15
  export declare function openSquare(squarePath: string, options?: Pick<SquareBuildOptions, 'clock' | 'notifier'>): Promise<OpenSquare>;
16
- export declare function probeSquare(squarePath: string): OpenSquare | undefined;
16
+ export declare function probeSquare(squarePath: string): Promise<OpenSquare | undefined>;
17
17
  export declare function buildSquare(squarePath: string, options: SquareBuildOptions): Promise<OpenSquare>;
18
18
  export declare function buildMemorySquare(options: SquareBuildOptions): OpenSquare;
19
19
  /** Wait for any bound artifact to change; delivery callers re-project after the edge. */
@@ -2,17 +2,13 @@ import fs from 'node:fs';
2
2
  import { createSquareState, probeSquareFile, writeSquareSnapshot, withSquareFileLock, openSquareCell, createMemoryCell, } from './square-storage.js';
3
3
  import { InternalSquareError, SquareError, } from './model.js';
4
4
  import { closeOpenSquare } from './open-square.js';
5
- /** The current CLI file mutation boundary. */
6
- function writeSquareState(squarePath, squareState) {
7
- writeSquareSnapshot(squarePath, squareState);
8
- }
9
5
  /** File-owned artifact creation for the CLI and path-backed public facade. */
10
6
  export async function createSquare(squarePath, options, snippet) {
11
- await withSquareFileLock(squarePath, () => {
12
- if (fs.existsSync(squarePath) && !options.force) {
7
+ await withSquareFileLock(squarePath, async () => {
8
+ if (await fs.promises.access(squarePath).then(() => true, () => false) && !options.force) {
13
9
  throw new InternalSquareError('conflict', `Refusing to overwrite existing square: ${squarePath}\nPass -f to overwrite.`);
14
10
  }
15
- writeSquareSnapshot(squarePath, createSquareState(options, snippet));
11
+ await writeSquareSnapshot(squarePath, await createSquareState(options, snippet));
16
12
  });
17
13
  }
18
14
  function validateBuildOptions(options) {
@@ -39,8 +35,8 @@ export async function openSquare(squarePath, options = {}) {
39
35
  throw error;
40
36
  }
41
37
  }
42
- export function probeSquare(squarePath) {
43
- const state = probeSquareFile(squarePath);
38
+ export async function probeSquare(squarePath) {
39
+ const state = await probeSquareFile(squarePath);
44
40
  return state === undefined ? undefined : { cell: createMemoryCell(state), clock: Date.now, location: squarePath };
45
41
  }
46
42
  export async function buildSquare(squarePath, options) {
@@ -7,10 +7,10 @@ import { type SquareState } from './model.js';
7
7
  * protects it.
8
8
  */
9
9
  export { createSquareState, };
10
- export declare function readSquareFile(squarePath: string): SquareState;
11
- export declare function probeSquareFile(squarePath: string): SquareState | undefined;
10
+ export declare function readSquareFile(squarePath: string): Promise<SquareState>;
11
+ export declare function probeSquareFile(squarePath: string): Promise<SquareState | undefined>;
12
12
  export declare function diagnoseSquareFile(squarePath: string): ReturnType<typeof diagnoseArtifactFile>;
13
- export declare function writeSquareSnapshot(squarePath: string, squareState: SquareState): void;
13
+ export declare function writeSquareSnapshot(squarePath: string, squareState: SquareState): Promise<void>;
14
14
  export declare function withSquareFileLock<T>(squarePath: string, fn: () => T | Promise<T>): Promise<T>;
15
15
  /** In-process cell for fast application tests and embedded consumers. */
16
16
  export declare function createMemoryCell(initial: SquareState): StateCell;
@@ -9,17 +9,17 @@ import { LOCK_RETRY_MS, LOCK_STALE_MS } from './runtime.js';
9
9
  * protects it.
10
10
  */
11
11
  export { createSquareState, };
12
- export function readSquareFile(squarePath) {
12
+ export async function readSquareFile(squarePath) {
13
13
  return loadSquare(squarePath);
14
14
  }
15
- export function probeSquareFile(squarePath) {
15
+ export async function probeSquareFile(squarePath) {
16
16
  return probeSquare(squarePath);
17
17
  }
18
- export function diagnoseSquareFile(squarePath) {
18
+ export async function diagnoseSquareFile(squarePath) {
19
19
  return diagnoseArtifactFile(squarePath);
20
20
  }
21
- export function writeSquareSnapshot(squarePath, squareState) {
22
- writeSquareFile(squarePath, squareState);
21
+ export async function writeSquareSnapshot(squarePath, squareState) {
22
+ await writeSquareFile(squarePath, squareState);
23
23
  }
24
24
  export function withSquareFileLock(squarePath, fn) {
25
25
  return withFileLock(`${squarePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, fn);
@@ -102,9 +102,9 @@ export function createMemoryCell(initial) {
102
102
  };
103
103
  return cell;
104
104
  }
105
- function fileFingerprint(squarePath) {
105
+ async function fileFingerprint(squarePath) {
106
106
  try {
107
- const stat = fs.statSync(squarePath);
107
+ const stat = await fs.promises.stat(squarePath);
108
108
  return `${stat.ino}:${stat.size}:${stat.mtimeMs}:${stat.ctimeMs}`;
109
109
  }
110
110
  catch {
@@ -115,36 +115,39 @@ function fileFingerprint(squarePath) {
115
115
  export function createFileCell(squarePath) {
116
116
  let closed = false;
117
117
  let version = 0;
118
- let fingerprint = fileFingerprint(squarePath);
118
+ let fingerprint;
119
119
  let cached;
120
- function observe() {
121
- const next = fileFingerprint(squarePath);
122
- if (next !== fingerprint) {
120
+ async function observe() {
121
+ const next = await fileFingerprint(squarePath);
122
+ if (fingerprint === undefined) {
123
+ fingerprint = next;
124
+ }
125
+ else if (next !== fingerprint) {
123
126
  fingerprint = next;
124
127
  cached = undefined;
125
128
  version += 1;
126
129
  }
127
130
  return fingerprint;
128
131
  }
129
- function currentState() {
130
- const observed = observe();
132
+ async function currentState() {
133
+ const observed = await observe();
131
134
  if (cached?.fingerprint === observed)
132
135
  return cloneState(cached.state);
133
- const decoded = readSquareFile(squarePath);
136
+ const decoded = await readSquareFile(squarePath);
134
137
  cached = { fingerprint: observed, state: cloneState(decoded) };
135
138
  return cloneState(cached.state);
136
139
  }
137
140
  return {
138
141
  async transact(fn) {
139
142
  assertCellOpen(closed);
140
- return withFileLock(`${squarePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
143
+ return withFileLock(`${squarePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, async () => {
141
144
  assertCellOpen(closed);
142
- const current = currentState();
145
+ const current = await currentState();
143
146
  const working = cloneState(current);
144
147
  const outcome = fn(working, version);
145
148
  if (outcome.state !== undefined) {
146
- writeSquareSnapshot(squarePath, outcome.state);
147
- fingerprint = fileFingerprint(squarePath);
149
+ await writeSquareSnapshot(squarePath, outcome.state);
150
+ fingerprint = await fileFingerprint(squarePath);
148
151
  cached = { fingerprint, state: cloneState(outcome.state) };
149
152
  version += 1;
150
153
  }
@@ -153,13 +156,13 @@ export function createFileCell(squarePath) {
153
156
  },
154
157
  async read() {
155
158
  assertCellOpen(closed);
156
- return { state: currentState(), version };
159
+ return { state: await currentState(), version };
157
160
  },
158
161
  async changed(sinceVersion, timeoutMs) {
159
162
  assertCellOpen(closed);
160
163
  const deadline = Date.now() + Math.max(0, timeoutMs);
161
164
  while (true) {
162
- observe();
165
+ await observe();
163
166
  if (version > sinceVersion)
164
167
  return true;
165
168
  const remaining = deadline - Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astrosheep/square",
3
- "version": "0.3.27",
3
+ "version": "0.3.28",
4
4
  "description": "A shared public square where agents join, catch activity, express, and step out when done.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,6 +52,10 @@
52
52
  "./paseo": {
53
53
  "types": "./dist/paseo.d.ts",
54
54
  "default": "./dist/paseo.js"
55
+ },
56
+ "./server": {
57
+ "types": "./dist/opencode.d.ts",
58
+ "default": "./dist/opencode.js"
55
59
  }
56
60
  },
57
61
  "main": "dist/index.js",
@@ -1,48 +0,0 @@
1
- import { presentPendingAtBoundary } from '../dist/boundary-presentation.js';
2
- import { automaticSessionEnd, automaticSessionStart } from '../dist/automatic-session.js';
3
-
4
- export default async function squareOpenCodePlugin() {
5
- const joining = new Map();
6
- return {
7
- event: async ({ event }) => {
8
- if (event.type === 'session.created' || event.type === 'session.updated') {
9
- const sessionID = event.properties?.sessionID;
10
- const cwd = event.properties?.info?.directory || process.cwd();
11
- if (sessionID) {
12
- try {
13
- const context = await automaticSessionStart('opencode', sessionID, cwd);
14
- if (context) joining.set(sessionID, context);
15
- } catch { /* startup remains bounded */ }
16
- }
17
- } else if (event.type === 'session.deleted') {
18
- const sessionID = event.properties?.sessionID;
19
- const cwd = event.properties?.info?.directory || process.cwd();
20
- if (sessionID) {
21
- joining.delete(sessionID);
22
- await automaticSessionEnd('opencode', sessionID, cwd);
23
- }
24
- }
25
- },
26
- 'shell.env': async (input, output) => {
27
- if (input.sessionID) output.env.OPENCODE_SESSION_ID = input.sessionID;
28
- },
29
-
30
- 'tool.execute.after': async (input, output) => {
31
- try {
32
- const joined = joining.get(input.sessionID);
33
- if (joined) {
34
- joining.delete(input.sessionID);
35
- output.output = `${output.output}${output.output === '' ? '' : '\n\n'}${joined}`;
36
- }
37
- await presentPendingAtBoundary(
38
- input.sessionID,
39
- (context) => {
40
- output.output = `${output.output}${output.output === '' ? '' : '\n\n'}${context}`;
41
- }
42
- );
43
- } catch {
44
- // A failed admission remains available at a later boundary.
45
- }
46
- },
47
- };
48
- }