@astrosheep/square 0.3.4 → 0.3.6

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 (52) hide show
  1. package/codex-plugin/.codex-plugin/plugin.json +3 -2
  2. package/dist/activity-feed.js +26 -18
  3. package/dist/activity.js +23 -22
  4. package/dist/artifact.js +126 -202
  5. package/dist/claude-hook.js +45 -21
  6. package/dist/cli/context.js +143 -0
  7. package/dist/cli/harness-command.js +50 -0
  8. package/dist/cli/maintenance-commands.js +76 -0
  9. package/dist/cli/meta-commands.js +28 -0
  10. package/dist/cli/observation-commands.js +453 -0
  11. package/dist/cli/program.js +48 -0
  12. package/dist/cli/registry.js +40 -0
  13. package/dist/cli/square-commands.js +219 -0
  14. package/dist/cmd/notify-once.js +23 -21
  15. package/dist/compact.js +6 -19
  16. package/dist/decisions.js +53 -86
  17. package/dist/delivery-health.js +104 -210
  18. package/dist/delivery.js +68 -18
  19. package/dist/doctor.js +9 -8
  20. package/dist/harness-claude.js +68 -0
  21. package/dist/harness-codex.js +119 -0
  22. package/dist/harness-links.js +123 -0
  23. package/dist/harness-stage.js +36 -0
  24. package/dist/harness.js +94 -576
  25. package/dist/help.js +44 -35
  26. package/dist/inbox.js +12 -11
  27. package/dist/index.js +30 -129
  28. package/dist/list.js +1 -1
  29. package/dist/model.js +0 -6
  30. package/dist/notification-failures.js +54 -0
  31. package/dist/notifications.js +47 -62
  32. package/dist/paseo-timeline.js +58 -188
  33. package/dist/presentation.js +55 -63
  34. package/dist/presented.js +9 -8
  35. package/dist/registry.js +55 -45
  36. package/dist/runtime.js +26 -137
  37. package/dist/square-application.js +264 -0
  38. package/dist/square-core.js +3 -11
  39. package/dist/square.js +5 -1362
  40. package/dist/stream.js +27 -126
  41. package/dist/wake-sink.js +134 -188
  42. package/dist/watch.js +79 -138
  43. package/extensions/square-opencode.js +1 -1
  44. package/extensions/square-pi.js +8 -130
  45. package/guides/architect.md +3 -3
  46. package/guides/participant.md +25 -16
  47. package/package.json +2 -2
  48. package/skills/brainstorm/SKILL.md +25 -32
  49. package/skills/square/.claude-plugin/plugin.json +1 -1
  50. package/skills/square/SKILL.md +39 -107
  51. package/skills/square-feedback/SKILL.md +4 -4
  52. package/dist/terminal.js +0 -125
@@ -0,0 +1,119 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { SQUARE_IDENTITY } from './identity.js';
6
+ import { stageReplacement } from './harness-stage.js';
7
+ export const CODEX_HOOK_COMMAND = SQUARE_IDENTITY.hookCommand;
8
+ export const SQUARE_CODEX_MARKER = SQUARE_IDENTITY.hookMarker;
9
+ export const CODEX_PLUGIN_ID = SQUARE_IDENTITY.pluginId;
10
+ export const CODEX_MARKETPLACE_NAME = SQUARE_IDENTITY.marketplaceName;
11
+ const LEGACY_MARKETPLACES = ['astrosheep-square'];
12
+ export function codexMarketplaceRoot(homeDir) {
13
+ return path.join(homeDir, '.square', 'codex', 'marketplaces', CODEX_MARKETPLACE_NAME);
14
+ }
15
+ export function codexPluginRoot(homeDir) { return path.join(codexMarketplaceRoot(homeDir), 'plugins', SQUARE_IDENTITY.pluginName); }
16
+ export function codexPluginHooksPath(homeDir) { return path.join(codexPluginRoot(homeDir), 'hooks', 'hooks.json'); }
17
+ function codexHome(homeDir) { return path.join(homeDir, '.codex'); }
18
+ export function codexHomeHooksPath(homeDir) { return path.join(codexHome(homeDir), 'hooks.json'); }
19
+ export function codexConfigPath(homeDir) { return path.join(codexHome(homeDir), 'config.toml'); }
20
+ function runCodex(homeDir, args) {
21
+ const result = spawnSync(process.env.SQUARE_CODEX_BIN || 'codex', args, {
22
+ encoding: 'utf8', env: { ...process.env, HOME: homeDir, CODEX_HOME: codexHome(homeDir) }, timeout: 30_000,
23
+ });
24
+ if (result.error)
25
+ throw result.error;
26
+ return { status: result.status ?? 1, stdout: result.stdout || '', stderr: result.stderr || '' };
27
+ }
28
+ function requireSuccess(result, operation, allowMissing = false) {
29
+ if (result.status === 0 || (allowMissing && /not configured|not installed|not found/i.test(result.stderr)))
30
+ return;
31
+ throw new Error(`Codex ${operation} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`);
32
+ }
33
+ export function upsertTomlSectionKey(text, section, key, value) {
34
+ const lines = text.replace(/\n$/, '').split('\n').filter((line, i) => line !== '' || i < text.length);
35
+ const header = `[${section}]`;
36
+ const start = lines.findIndex((line) => line.trim() === header);
37
+ if (start < 0)
38
+ return `${text.trimEnd()}${text.trim() ? '\n\n' : ''}${header}\n${key} = ${value}\n`;
39
+ const end = lines.findIndex((line, i) => i > start && /^\s*\[[^\]]+\]/.test(line));
40
+ const stop = end < 0 ? lines.length : end;
41
+ const match = new RegExp(`^\\s*${key.replace(/[.*+?^${}()|[\\]\\]/g, '\\$&')}\\s*=`);
42
+ const existing = lines.findIndex((line, i) => i > start && i < stop && match.test(line));
43
+ if (existing < 0)
44
+ lines.splice(stop, 0, `${key} = ${value}`);
45
+ else
46
+ lines[existing] = `${key} = ${value}`;
47
+ return `${lines.join('\n')}\n`;
48
+ }
49
+ function writeAtomic(file, text) {
50
+ fs.mkdirSync(path.dirname(file), { recursive: true });
51
+ const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
52
+ fs.writeFileSync(temp, text, { mode: 0o600 });
53
+ fs.renameSync(temp, file);
54
+ }
55
+ export async function installCodexPlugin(homeDir, run = runCodex) {
56
+ const root = codexMarketplaceRoot(homeDir);
57
+ const config = codexConfigPath(homeDir);
58
+ const current = fs.existsSync(config) ? fs.readFileSync(config, 'utf8') : '';
59
+ const staged = stageReplacement(root, (stage) => {
60
+ const plugin = path.join(stage, 'plugins', SQUARE_IDENTITY.pluginName);
61
+ fs.cpSync(fileURLToPath(new URL('../codex-plugin/', import.meta.url)), plugin, { recursive: true });
62
+ const skill = path.join(plugin, 'skills', 'square', 'SKILL.md');
63
+ fs.mkdirSync(path.dirname(skill), { recursive: true });
64
+ fs.copyFileSync(path.join(fileURLToPath(new URL('../skills/square/', import.meta.url)), 'SKILL.md'), skill);
65
+ writeAtomic(path.join(stage, '.agents', 'plugins', 'marketplace.json'), `${JSON.stringify({ name: CODEX_MARKETPLACE_NAME, plugins: [{ name: SQUARE_IDENTITY.pluginName, source: { source: 'local', path: './plugins/square' } }] }, null, 2)}\n`);
66
+ });
67
+ const notes = [];
68
+ try {
69
+ writeAtomic(config, upsertTomlSectionKey(current, 'features', 'hooks', 'true'));
70
+ requireSuccess(run(homeDir, ['plugin', 'marketplace', 'add', root, '--json']), 'marketplace install', true);
71
+ const installed = run(homeDir, ['plugin', 'add', CODEX_PLUGIN_ID, '--json']);
72
+ requireSuccess(installed, 'plugin install');
73
+ let installedPath;
74
+ try {
75
+ const value = JSON.parse(installed.stdout).installedPath;
76
+ if (typeof value === 'string')
77
+ installedPath = value;
78
+ }
79
+ catch {
80
+ notes.push('Codex returned non-JSON installation output.');
81
+ }
82
+ for (const marketplace of LEGACY_MARKETPLACES) {
83
+ const pluginId = `${SQUARE_IDENTITY.pluginName}@${marketplace}`;
84
+ if (!current.includes(`[marketplaces.${marketplace}]`) && !current.includes(`[plugins."${pluginId}"]`))
85
+ continue;
86
+ requireSuccess(run(homeDir, ['plugin', 'remove', pluginId, '--json']), 'legacy plugin removal', true);
87
+ requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', marketplace, '--json']), 'legacy marketplace removal', true);
88
+ notes.push(`retired ${pluginId}`);
89
+ }
90
+ staged.finalize();
91
+ return { configPath: config, marketplaceRoot: root, pluginRoot: codexPluginRoot(homeDir), ...(installedPath ? { installedPath } : {}), notes };
92
+ }
93
+ catch (error) {
94
+ staged.rollback();
95
+ throw error;
96
+ }
97
+ }
98
+ export async function uninstallCodexPlugin(homeDir, run = runCodex) {
99
+ requireSuccess(run(homeDir, ['plugin', 'remove', CODEX_PLUGIN_ID, '--json']), 'plugin removal', true);
100
+ requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', CODEX_MARKETPLACE_NAME, '--json']), 'marketplace removal', true);
101
+ for (const marketplace of LEGACY_MARKETPLACES) {
102
+ requireSuccess(run(homeDir, ['plugin', 'remove', `${SQUARE_IDENTITY.pluginName}@${marketplace}`, '--json']), 'legacy plugin removal', true);
103
+ requireSuccess(run(homeDir, ['plugin', 'marketplace', 'remove', marketplace, '--json']), 'legacy marketplace removal', true);
104
+ }
105
+ const root = codexMarketplaceRoot(homeDir);
106
+ fs.rmSync(root, { recursive: true, force: true });
107
+ fs.rmSync(codexHomeHooksPath(homeDir), { force: true });
108
+ return { paths: [root, codexConfigPath(homeDir), codexHomeHooksPath(homeDir)], notes: [] };
109
+ }
110
+ export async function doctorCodexPlugin(homeDir, run = runCodex) {
111
+ const root = codexMarketplaceRoot(homeDir);
112
+ const config = codexConfigPath(homeDir);
113
+ const listed = run(homeDir, ['plugin', 'list', '--json']);
114
+ return [
115
+ /^hooks\s*=\s*true$/m.test(fs.existsSync(config) ? fs.readFileSync(config, 'utf8') : '') ? `✓ features.hooks=true in ${config}` : `○ features.hooks missing in ${config}`,
116
+ fs.existsSync(codexPluginHooksPath(homeDir)) ? `✓ Square plugin hooks ${codexPluginHooksPath(homeDir)}` : `○ Square plugin bundle missing ${root}`,
117
+ listed.status === 0 && listed.stdout.includes(CODEX_PLUGIN_ID) ? `✓ ${CODEX_PLUGIN_ID} installed` : `○ ${CODEX_PLUGIN_ID} unavailable`,
118
+ ];
119
+ }
@@ -0,0 +1,123 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
+ function packageRoot() {
7
+ // Emitted modules live in dist; package assets are one level above them.
8
+ return fileURLToPath(new URL('../', import.meta.url));
9
+ }
10
+ function lstatMaybe(target) {
11
+ try {
12
+ return fs.lstatSync(target);
13
+ }
14
+ catch (error) {
15
+ if (error.code === 'ENOENT')
16
+ return undefined;
17
+ throw error;
18
+ }
19
+ }
20
+ export function installHarnessLinks(links, force = false) {
21
+ const prepared = links.map((link) => {
22
+ if (!fs.existsSync(link.source)) {
23
+ throw new Error(`Harness link source is missing: ${link.source}`);
24
+ }
25
+ return {
26
+ ...link,
27
+ existing: lstatMaybe(link.target),
28
+ sourceIsDirectory: fs.statSync(link.source).isDirectory(),
29
+ };
30
+ });
31
+ for (const { target, existing } of prepared) {
32
+ if (existing !== undefined && !force)
33
+ throw new Error(`Refusing to overwrite existing link: ${target}\nPass -f to replace it.`);
34
+ }
35
+ for (const { source, target, existing, sourceIsDirectory } of prepared) {
36
+ fs.mkdirSync(path.dirname(target), { recursive: true });
37
+ if (existing !== undefined)
38
+ fs.rmSync(target, { recursive: true, force: true });
39
+ const symlinkType = os.platform() === 'win32' && sourceIsDirectory ? 'junction' : 'file';
40
+ fs.symlinkSync(source, target, symlinkType);
41
+ }
42
+ return prepared.map(({ target }) => target);
43
+ }
44
+ function sameLink(source, target) {
45
+ try {
46
+ return fs.realpathSync(source) === fs.realpathSync(target);
47
+ }
48
+ catch {
49
+ return false;
50
+ }
51
+ }
52
+ export function uninstallHarnessLinks(links) {
53
+ const removed = [];
54
+ for (const link of links) {
55
+ try {
56
+ if (fs.lstatSync(link.target).isSymbolicLink() && sameLink(link.source, link.target)) {
57
+ fs.rmSync(link.target, { force: true });
58
+ removed.push(link.target);
59
+ }
60
+ }
61
+ catch {
62
+ // A missing or user-owned target is preserved.
63
+ }
64
+ }
65
+ return removed;
66
+ }
67
+ export function doctorHarnessLinks(links) {
68
+ return links.map((link) => sameLink(link.source, link.target)
69
+ ? `✓ Square ${link.kind ?? 'link'} ${link.target}`
70
+ : `○ Square ${link.kind ?? 'link'} missing ${link.target}`);
71
+ }
72
+ function runOpenCode(homeDir, args) {
73
+ const result = spawnSync(process.env.SQUARE_OPENCODE_BIN || 'opencode', args, {
74
+ encoding: 'utf8',
75
+ env: { ...process.env, HOME: homeDir, XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME ?? path.join(homeDir, '.config') },
76
+ timeout: 30_000,
77
+ });
78
+ if (result.error)
79
+ throw result.error;
80
+ return { status: result.status ?? 1, stdout: result.stdout || '', stderr: result.stderr || '' };
81
+ }
82
+ /** Verify that OpenCode accepts its resolved runtime configuration after links are installed. */
83
+ export function verifyOpenCodeRuntime(homeDir, run = runOpenCode) {
84
+ try {
85
+ const result = run(homeDir, ['debug', 'config']);
86
+ if (result.status !== 0) {
87
+ return `✕ OpenCode debug config failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`;
88
+ }
89
+ let config;
90
+ try {
91
+ config = JSON.parse(result.stdout);
92
+ }
93
+ catch {
94
+ return '✕ OpenCode debug config returned invalid JSON';
95
+ }
96
+ const plugin = config.config?.plugin;
97
+ const expected = pathToFileURL(opencodeExtensionLink(homeDir).target).href;
98
+ if (Array.isArray(plugin) && plugin.includes(expected))
99
+ return '✓ OpenCode debug config loaded';
100
+ return `○ OpenCode plugin not loaded: ${expected}`;
101
+ }
102
+ catch (error) {
103
+ return `○ OpenCode runtime unavailable (${error instanceof Error ? error.message : String(error)})`;
104
+ }
105
+ }
106
+ export function skillLinks(homeDir = os.homedir(), parents = ['.claude', '.agents']) {
107
+ return parents.flatMap((parent) => ['square', 'brainstorm'].map((name) => ({
108
+ source: path.join(packageRoot(), 'skills', name),
109
+ target: path.join(homeDir, parent, 'skills', name),
110
+ kind: 'skill',
111
+ })));
112
+ }
113
+ export function opencodeExtensionLink(homeDir = os.homedir()) {
114
+ const configHome = process.env.XDG_CONFIG_HOME ?? path.join(homeDir, '.config');
115
+ return {
116
+ source: path.join(packageRoot(), 'extensions', 'square-opencode.js'),
117
+ target: path.join(configHome, 'opencode', 'plugins', 'square.js'),
118
+ kind: 'extension',
119
+ };
120
+ }
121
+ export function piExtensionLink(homeDir = os.homedir()) {
122
+ return { source: path.join(packageRoot(), 'extensions', 'square-pi.js'), target: path.join(homeDir, '.pi', 'agent', 'extensions', 'square.js'), kind: 'extension' };
123
+ }
@@ -0,0 +1,36 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /** Replace a file or directory while retaining the previous value for rollback. */
4
+ export function stageReplacement(root, populate) {
5
+ const token = `${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}`;
6
+ const stage = `${root}.${token}.stage`;
7
+ const backup = `${root}.${token}.previous`;
8
+ let replaced = false;
9
+ let hadOriginal = false;
10
+ fs.mkdirSync(path.dirname(root), { recursive: true });
11
+ try {
12
+ populate(stage);
13
+ hadOriginal = fs.existsSync(root);
14
+ if (hadOriginal)
15
+ fs.renameSync(root, backup);
16
+ fs.renameSync(stage, root);
17
+ replaced = true;
18
+ }
19
+ catch (error) {
20
+ fs.rmSync(stage, { recursive: true, force: true });
21
+ if (hadOriginal && fs.existsSync(backup) && !fs.existsSync(root))
22
+ fs.renameSync(backup, root);
23
+ throw error;
24
+ }
25
+ return {
26
+ rollback() {
27
+ if (replaced)
28
+ fs.rmSync(root, { recursive: true, force: true });
29
+ if (hadOriginal && fs.existsSync(backup))
30
+ fs.renameSync(backup, root);
31
+ },
32
+ finalize() {
33
+ fs.rmSync(backup, { recursive: true, force: true });
34
+ },
35
+ };
36
+ }