@ravefox/space 0.2.1 → 0.4.0

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 (167) hide show
  1. package/LICENSE +21 -205
  2. package/README.md +104 -31
  3. package/bin/space.js +113 -0
  4. package/lib/agents.js +181 -0
  5. package/lib/boardTools.js +89 -0
  6. package/lib/credentials.js +95 -0
  7. package/lib/daemon.js +294 -0
  8. package/lib/harness.js +166 -0
  9. package/lib/http.js +101 -0
  10. package/lib/login.js +68 -0
  11. package/lib/mcp.js +39 -0
  12. package/lib/runtime.js +253 -0
  13. package/lib/store.js +96 -0
  14. package/lib/tools.js +93 -0
  15. package/lib/workspaceSandbox.js +77 -0
  16. package/lib/workspaceTools.js +160 -0
  17. package/lib/workspaceWorker.js +17 -0
  18. package/package.json +27 -46
  19. package/NOTICE +0 -55
  20. package/assets/webhook-sdk/python/bot.py +0 -63
  21. package/assets/webhook-sdk/python/commonly.py +0 -232
  22. package/node_modules/@commonlyai/seats/README.md +0 -25
  23. package/node_modules/@commonlyai/seats/package.json +0 -22
  24. package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
  25. package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
  26. package/node_modules/@commonlyai/seats/src/contract.js +0 -48
  27. package/node_modules/@commonlyai/seats/src/failures.js +0 -44
  28. package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
  29. package/node_modules/@commonlyai/seats/src/index.js +0 -22
  30. package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
  31. package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
  32. package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
  33. package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
  34. package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
  35. package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
  36. package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
  37. package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
  38. package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
  39. package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
  40. package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
  41. package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
  42. package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
  43. package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
  44. package/skills/commonly/SKILL.md +0 -233
  45. package/src/commands/agent.js +0 -6334
  46. package/src/commands/daemon.js +0 -933
  47. package/src/commands/dev.js +0 -153
  48. package/src/commands/doctor.js +0 -141
  49. package/src/commands/inbox.js +0 -330
  50. package/src/commands/login.js +0 -194
  51. package/src/commands/mcp.js +0 -56
  52. package/src/commands/pod.js +0 -120
  53. package/src/commands/task.js +0 -279
  54. package/src/commands/verify.js +0 -48
  55. package/src/index.js +0 -99
  56. package/src/lib/adapter-login.js +0 -76
  57. package/src/lib/adapters/broker-bridge-entry.js +0 -76
  58. package/src/lib/adapters/claude.js +0 -2207
  59. package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
  60. package/src/lib/adapters/codex.js +0 -1898
  61. package/src/lib/adapters/dsh.js +0 -513
  62. package/src/lib/adapters/goose.js +0 -510
  63. package/src/lib/adapters/hermes.js +0 -18
  64. package/src/lib/adapters/index.js +0 -68
  65. package/src/lib/adapters/opencode.js +0 -1681
  66. package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
  67. package/src/lib/adapters/pi-mcp-client.mjs +0 -617
  68. package/src/lib/adapters/pi-shell-env.mjs +0 -41
  69. package/src/lib/adapters/pi.js +0 -1025
  70. package/src/lib/adapters/qwen.js +0 -523
  71. package/src/lib/adapters/seat-model.js +0 -8
  72. package/src/lib/adapters/stub.js +0 -1
  73. package/src/lib/adapters/tested-version.js +0 -106
  74. package/src/lib/api.js +0 -205
  75. package/src/lib/claim-outcome.js +0 -128
  76. package/src/lib/claimed-work-gate.js +0 -73
  77. package/src/lib/cli-hint.js +0 -33
  78. package/src/lib/commit-attribution.js +0 -391
  79. package/src/lib/config.js +0 -155
  80. package/src/lib/content-refusal.js +0 -94
  81. package/src/lib/credential-file.js +0 -208
  82. package/src/lib/daemon-agent-states.js +0 -95
  83. package/src/lib/daemon-hint.js +0 -18
  84. package/src/lib/daemon-logs.js +0 -43
  85. package/src/lib/daemon-service-windows.js +0 -263
  86. package/src/lib/daemon-service.js +0 -486
  87. package/src/lib/daemon-state.js +0 -84
  88. package/src/lib/daemon-store.js +0 -119
  89. package/src/lib/daemon-supervisor.js +0 -629
  90. package/src/lib/daemon-watch.js +0 -115
  91. package/src/lib/declared-mcp-guard.js +0 -284
  92. package/src/lib/default-environment.js +0 -207
  93. package/src/lib/device-login.js +0 -126
  94. package/src/lib/digest-frame.js +0 -151
  95. package/src/lib/doctor.js +0 -291
  96. package/src/lib/egress-proxy.js +0 -160
  97. package/src/lib/enforcement.js +0 -946
  98. package/src/lib/environment.js +0 -681
  99. package/src/lib/first-output-watchdog.js +0 -191
  100. package/src/lib/git-readonly-server.js +0 -138
  101. package/src/lib/git-readonly.js +0 -463
  102. package/src/lib/git-remote-commonly.mjs +0 -100
  103. package/src/lib/grant-broker-guard.js +0 -345
  104. package/src/lib/hooks-config.js +0 -238
  105. package/src/lib/host-gh-broker.js +0 -152
  106. package/src/lib/host-gh-shim.mjs +0 -98
  107. package/src/lib/inbox.js +0 -483
  108. package/src/lib/instance-proxy.js +0 -76
  109. package/src/lib/machine-host.js +0 -29
  110. package/src/lib/mcp-credential-delivery.js +0 -121
  111. package/src/lib/mcp-first-run.js +0 -50
  112. package/src/lib/mcp-home.js +0 -644
  113. package/src/lib/mcp-ready.js +0 -68
  114. package/src/lib/mcp-server-version.js +0 -123
  115. package/src/lib/mcp-token.js +0 -36
  116. package/src/lib/mcp-warm-child.mjs +0 -412
  117. package/src/lib/memory-bridge.js +0 -191
  118. package/src/lib/memory-import.js +0 -156
  119. package/src/lib/missing-harness.js +0 -31
  120. package/src/lib/model-unavailable.js +0 -154
  121. package/src/lib/native-runtime-inventory.js +0 -40
  122. package/src/lib/outsider-grant-mcp.js +0 -82
  123. package/src/lib/own-key-launch.js +0 -42
  124. package/src/lib/pod-catch-up.js +0 -55
  125. package/src/lib/pod-focus.js +0 -224
  126. package/src/lib/pod-tail.js +0 -68
  127. package/src/lib/poll-liveness.js +0 -103
  128. package/src/lib/poll-retry.js +0 -81
  129. package/src/lib/poller.js +0 -200
  130. package/src/lib/provider-proxy.js +0 -42
  131. package/src/lib/public-seat-mcp-flag.js +0 -36
  132. package/src/lib/public-tool-error.js +0 -60
  133. package/src/lib/public-withheld-tools.js +0 -48
  134. package/src/lib/reply-receipt.js +0 -55
  135. package/src/lib/resolve-on-path.js +0 -22
  136. package/src/lib/runtime-inventory.js +0 -56
  137. package/src/lib/runtime-observation.js +0 -37
  138. package/src/lib/sandbox/bwrap.js +0 -275
  139. package/src/lib/sandbox/codex-perimeter.js +0 -16
  140. package/src/lib/sandbox/host-check.js +0 -268
  141. package/src/lib/sandbox/mode.js +0 -49
  142. package/src/lib/sandbox/opencode-loopback.mjs +0 -68
  143. package/src/lib/sandbox/seatbelt.js +0 -684
  144. package/src/lib/sandbox/turn-dir.js +0 -56
  145. package/src/lib/sandbox/windows-unsandboxed.js +0 -183
  146. package/src/lib/sandbox/workspace-git-config.js +0 -243
  147. package/src/lib/sandbox/workspace-pin.js +0 -158
  148. package/src/lib/seat-runtime-controls.js +0 -61
  149. package/src/lib/seat-stop.js +0 -97
  150. package/src/lib/server-skills.js +0 -32
  151. package/src/lib/session-recycle.js +0 -219
  152. package/src/lib/session-store.js +0 -215
  153. package/src/lib/skill-prompt.js +0 -74
  154. package/src/lib/skills-import.js +0 -113
  155. package/src/lib/spawn-credential.js +0 -402
  156. package/src/lib/spawn-retry.js +0 -311
  157. package/src/lib/stable-install.js +0 -176
  158. package/src/lib/supervised-turn.js +0 -352
  159. package/src/lib/task.js +0 -235
  160. package/src/lib/turn-failure-notice.js +0 -177
  161. package/src/lib/unconfined-seat-guard.js +0 -94
  162. package/src/lib/update-check.js +0 -192
  163. package/src/lib/upstream-refusal.js +0 -1
  164. package/src/lib/webhook-server.js +0 -109
  165. package/src/lib/windows-command.js +0 -393
  166. package/src/lib/workspace-git.js +0 -87
  167. package/src/lib/workspace-trust.js +0 -162
@@ -1,153 +0,0 @@
1
- /**
2
- * commonly dev <subcommand>
3
- *
4
- * up — start local Commonly instance (wraps ./dev.sh up)
5
- * clawdbot — refuses: the local OpenClaw gateway was retired (ADR-021 Part B)
6
- * down — stop local instance
7
- * logs — tail local instance logs
8
- * test — run backend tests
9
- *
10
- * Sets --instance http://localhost:5000 automatically after `dev up`.
11
- */
12
-
13
- import { spawnSync, spawn } from 'child_process';
14
- import { existsSync } from 'fs';
15
- import { join } from 'path';
16
- import { saveInstance, getToken, LOCAL_URL } from '../lib/config.js';
17
- import { createClient } from '../lib/api.js';
18
- import { CLI } from '../lib/cli-hint.js';
19
-
20
- const findDevSh = (startDir = process.cwd()) => {
21
- let dir = startDir;
22
- for (let i = 0; i < 8; i++) {
23
- const candidate = join(dir, 'dev.sh');
24
- if (existsSync(candidate)) return candidate;
25
- const parent = join(dir, '..');
26
- if (parent === dir) break;
27
- dir = parent;
28
- }
29
- return null;
30
- };
31
-
32
- const runDevSh = (args, opts = {}) => {
33
- const devSh = findDevSh();
34
- if (!devSh) {
35
- console.error('dev.sh not found — run this command from within the commonly repo');
36
- process.exit(1);
37
- }
38
-
39
- const spawnOpts = {
40
- stdio: 'inherit',
41
- env: { ...process.env, ...(opts.env || {}) },
42
- };
43
-
44
- if (opts.stream) {
45
- return spawn('bash', [devSh, ...args], spawnOpts);
46
- }
47
-
48
- return spawnSync('bash', [devSh, ...args], spawnOpts);
49
- };
50
-
51
- const ensureLocalInstanceConfig = () => {
52
- saveInstance({
53
- key: 'local',
54
- url: LOCAL_URL,
55
- token: getToken('local') || null,
56
- username: null,
57
- userId: null,
58
- });
59
- };
60
-
61
- export const registerDev = (program) => {
62
- const dev = program.command('dev').description('Local development environment');
63
-
64
- dev.addHelpText('after', `
65
- Examples:
66
- $ ${CLI} dev up # Start a local docker-compose stack
67
- $ ${CLI} dev status # Check health
68
- $ ${CLI} dev logs backend # Tail backend logs
69
- $ ${CLI} dev down # Stop everything
70
-
71
- Login against the local instance with:
72
- $ ${CLI} login --instance http://localhost:5000
73
- `);
74
-
75
- dev
76
- .command('up')
77
- .description('Start local Space instance')
78
- .action(async () => {
79
- runDevSh(['up']);
80
-
81
- ensureLocalInstanceConfig();
82
-
83
- console.log('\nLocal instance ready:');
84
- console.log(' Frontend: http://localhost:3000');
85
- console.log(' Backend: http://localhost:5000');
86
- console.log('\nLogin to local instance:');
87
- console.log(` ${CLI} login --instance http://localhost:5000 --key local`);
88
- });
89
-
90
- dev
91
- .command('clawdbot')
92
- .description('Retired: the local OpenClaw gateway is gone (ADR-021 Part B)')
93
- .allowUnknownOption()
94
- .allowExcessArguments()
95
- .action(() => {
96
- console.error(`The local OpenClaw gateway was retired (ADR-021 Part B), so \`${CLI} dev clawdbot\` no longer does anything.`);
97
- console.error(`Use native agents, or attach a local CLI with: ${CLI} agent attach <cli>`);
98
- process.exit(1);
99
- });
100
-
101
- dev
102
- .command('down')
103
- .description('Stop local Space instance')
104
- .action(() => {
105
- runDevSh(['down']);
106
- });
107
-
108
- dev
109
- .command('logs [service]')
110
- .description('Tail logs (backend, frontend, mongo, postgres)')
111
- .option('--follow', 'Stream logs continuously', true)
112
- .action((service) => {
113
- const args = service ? ['logs', service] : ['logs'];
114
- runDevSh(args, { stream: true });
115
- });
116
-
117
- dev
118
- .command('test')
119
- .description('Run tests')
120
- .option('--watch', 'Watch mode', false)
121
- .option('--frontend', 'Frontend tests only', false)
122
- .option('--backend', 'Backend tests only', false)
123
- .action((opts) => {
124
- const devSh = findDevSh();
125
- if (!devSh) { console.error('dev.sh not found'); process.exit(1); }
126
-
127
- if (!opts.frontend) runDevSh(['test']);
128
- if (!opts.backend) {
129
- const dir = join(findDevSh(), '../frontend');
130
- if (existsSync(dir)) {
131
- spawnSync('npm', ['test', ...(opts.watch ? [] : ['--', '--watchAll=false'])], {
132
- cwd: dir,
133
- stdio: 'inherit',
134
- shell: true,
135
- });
136
- }
137
- }
138
- });
139
-
140
- dev
141
- .command('status')
142
- .description('Check health of local instance')
143
- .action(async () => {
144
- const client = createClient({ instance: LOCAL_URL, token: null });
145
- try {
146
- const data = await client.get('/api/health');
147
- console.log('Local instance: healthy');
148
- console.log(JSON.stringify(data, null, 2));
149
- } catch {
150
- console.log(`Local instance: not running (start with: ${CLI} dev up)`);
151
- }
152
- });
153
- };
@@ -1,141 +0,0 @@
1
- /**
2
- * commonly doctor — one line per check, exit 1 when any fails (TASK-660).
3
- * The checks live in ../lib/doctor.js; this file supplies the real host.
4
- */
5
- import { accessSync, constants, existsSync, readFileSync, statSync } from 'fs';
6
- import { spawnSync } from 'child_process';
7
- import { homedir } from 'os';
8
- import { join } from 'path';
9
- import { createClient } from '../lib/api.js';
10
- import { getToken, resolveInstance, resolveInstanceUrl } from '../lib/config.js';
11
- import { loadDaemonRecord } from '../lib/daemon-store.js';
12
- import { servicePaths } from '../lib/daemon-service.js';
13
- import { getAdapter } from '../lib/adapters/index.js';
14
- import { runLoginStatus, signedOutAdapters } from '../lib/adapter-login.js';
15
- import { detectSeatbelt } from '../lib/sandbox/seatbelt.js';
16
- import { daemonStatusErrorMessage, getDaemonMachineStatus } from './daemon.js';
17
- import { listLocalAgents, listUnreadableAgentTokens } from './agent.js';
18
- import { doctorExitCode, formatDoctorLine, runDoctor } from '../lib/doctor.js';
19
- import { CLI } from '../lib/cli-hint.js';
20
-
21
- const REQUEST_TIMEOUT_MS = 5000;
22
-
23
- // Every server read doctor makes is bounded, so a hung instance is a fail line, not a hang.
24
- export const timedClient = (client) => ({
25
- ...client,
26
- get: (path, params = {}, options = {}) => client.get(path, params, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), ...options }),
27
- });
28
-
29
- const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
30
-
31
- const isExecutable = (path) => {
32
- try {
33
- if (!statSync(path).isFile()) return false;
34
- accessSync(path, constants.X_OK);
35
- return true;
36
- } catch {
37
- return false;
38
- }
39
- };
40
-
41
- const isFile = (path) => {
42
- try {
43
- return statSync(path).isFile();
44
- } catch {
45
- return false;
46
- }
47
- };
48
-
49
- // Node refuses to spawn a .cmd without a shell (CVE-2024-27980), and npm's
50
- // Windows shim is a .cmd, so that one case goes through cmd.exe, quoted.
51
- export const versionCommand = (binary, platform = process.platform) => (
52
- platform === 'win32' && /\.(cmd|bat)$/i.test(binary)
53
- ? { command: `"${binary}" --version`, args: [], shell: true }
54
- : { command: binary, args: ['--version'], shell: false }
55
- );
56
-
57
- const runVersion = (binary) => {
58
- const { command, args, shell } = versionCommand(binary);
59
- const res = spawnSync(command, args, { encoding: 'utf8', timeout: REQUEST_TIMEOUT_MS, shell });
60
- if (res.error || res.status !== 0) return null;
61
- return (res.stdout || '').trim().split(/\s+/)[0] || null;
62
- };
63
-
64
- /**
65
- * The instance and token every other command would use for `instanceArg`:
66
- * createClient's own resolution (api.js), so doctor cannot pass a login that
67
- * `agent attach` then fails, or fail one that works. COMMONLY_TOKEN wins over
68
- * the saved token there, so it wins here, and the line says which one it sent.
69
- */
70
- export const loginTarget = ({
71
- instanceArg = null,
72
- env = process.env,
73
- resolve = resolveInstance,
74
- resolveUrl = resolveInstanceUrl,
75
- tokenFor = getToken,
76
- } = {}) => {
77
- const saved = resolve(instanceArg);
78
- const token = tokenFor(instanceArg);
79
- return {
80
- key: saved?.key || null,
81
- url: resolveUrl(instanceArg),
82
- tokenSource: !token ? null : (env.COMMONLY_TOKEN ? 'env' : 'saved'),
83
- loginHint: instanceArg ? `${CLI} login --instance ${instanceArg}` : `${CLI} login`,
84
- };
85
- };
86
-
87
- export const hostDoctorDeps = ({ instance: instanceArg = null } = {}) => ({
88
- listAgents: () => listLocalAgents(),
89
- listUnreadable: () => listUnreadableAgentTokens(),
90
- getAdapter,
91
- node: { nodeVersion: process.versions.node, enginesRange: pkg.engines?.node },
92
- install: {
93
- pathValue: process.env.PATH,
94
- platform: process.platform,
95
- isExecutable,
96
- runVersion,
97
- runningVersion: pkg.version,
98
- },
99
- login: {
100
- target: loginTarget({ instanceArg }),
101
- fetchUser: () => timedClient(createClient({ instance: instanceArg })).get('/api/auth/user'),
102
- },
103
- daemon: {
104
- loadRecord: loadDaemonRecord,
105
- serviceFile: servicePaths().file,
106
- fileExists: existsSync,
107
- fetchMachine: (record) => getDaemonMachineStatus({
108
- client: timedClient(createClient({ instance: record.instanceUrl, token: record.daemonToken })),
109
- record,
110
- }),
111
- describeError: daemonStatusErrorMessage,
112
- now: Date.now(),
113
- },
114
- adapterLogin: () => signedOutAdapters({
115
- detect: (name) => getAdapter(name)?.detect(),
116
- runStatus: runLoginStatus,
117
- }),
118
- macos: {
119
- platform: process.platform,
120
- detectSeatbelt,
121
- isFile,
122
- loginKeychainPath: join(homedir(), 'Library', 'Keychains', 'login.keychain-db'),
123
- },
124
- });
125
-
126
- export const registerDoctor = (program) => {
127
- program
128
- .command('doctor')
129
- .description('Check this computer: Node, the install, login, the daemon, adapter CLIs, macOS prerequisites')
130
- .option('--instance <keyOrUrl>', 'Saved instance to check the login of (default: the active one)')
131
- .action(async (opts) => {
132
- try {
133
- const results = await runDoctor(hostDoctorDeps({ instance: opts.instance || null }));
134
- for (const line of results.map(formatDoctorLine)) console.log(line);
135
- process.exitCode = doctorExitCode(results);
136
- } catch (error) {
137
- console.error(`${CLI} doctor could not finish: ${error.message}`);
138
- process.exitCode = 1;
139
- }
140
- });
141
- };
@@ -1,330 +0,0 @@
1
- /**
2
- * commonly inbox <subcommand>
3
- *
4
- * list — read ONE account's own attention queue, newest first, optionally only what is new
5
- * ack — acknowledge an item (takes the item's `attentionItemId`)
6
- * choose — rule a decision card (takes the item's SOURCE id, i.e. the decision id)
7
- *
8
- * The account comes from `--token-file <path>` (or COMMONLY_TOKEN_FILE) and from
9
- * nowhere else — see the header of ../lib/inbox.js for why that is a rule and
10
- * not a preference. Every run prints the resolved account on its first line.
11
- */
12
-
13
- import { resolveInstanceUrl } from '../lib/config.js';
14
- import {
15
- InboxRefusal,
16
- createInboxRequest,
17
- countsLine,
18
- fetchQueue,
19
- filterItems,
20
- formatAccountLine,
21
- formatItemLine,
22
- nextCursorFrom,
23
- parseCursor,
24
- parseWindowMs,
25
- readCursorFile,
26
- readInboxToken,
27
- resolveAccountLabel,
28
- resolveTokenFilePath,
29
- writeCursorFile,
30
- INBOX_KINDS,
31
- TOKEN_FILE_ENV,
32
- } from '../lib/inbox.js';
33
- import { CLI } from '../lib/cli-hint.js';
34
-
35
- export const INBOX_TOKEN_HELP = `Account token file (or ${TOKEN_FILE_ENV})`;
36
-
37
- const normalizeKind = (value) => {
38
- if (value === undefined || value === null || value === '') return null;
39
- const kind = String(value).trim().toLowerCase();
40
- if (!INBOX_KINDS.includes(kind)) {
41
- throw new InboxRefusal(`Unknown --kind ${value}. One of: ${INBOX_KINDS.join(', ')}.`);
42
- }
43
- return kind;
44
- };
45
-
46
- /**
47
- * Resolve identity and build the request layer. Deliberately the only path to a
48
- * request: every subcommand goes through it, so no subcommand can grow its own
49
- * fallback to the saved login.
50
- */
51
- const connect = async (opts, { env, fetchImpl, sleep }) => {
52
- const tokenFile = resolveTokenFilePath(opts, env);
53
- if (!tokenFile) {
54
- throw new InboxRefusal(
55
- `no token file. Pass --token-file <path> or set ${TOKEN_FILE_ENV}.\n`
56
- + 'This command reads ONE account\'s own queue; it never falls back to the saved login, because on a\n'
57
- + 'shared operator host that login belongs to someone else (Sam\'s queue is the wrong queue, silently).',
58
- );
59
- }
60
- const token = readInboxToken(tokenFile);
61
- const baseUrl = resolveInstanceUrl(opts.instance);
62
- const request = createInboxRequest({
63
- baseUrl, token, tokenFile, fetchImpl, sleep,
64
- });
65
- const user = await request('/api/auth/user');
66
- return {
67
- tokenFile, baseUrl, request, user, account: resolveAccountLabel(user),
68
- };
69
- };
70
-
71
- const reportRefusal = (err, error) => {
72
- error(`refusing: ${err.message}`);
73
- return 1;
74
- };
75
-
76
- const reportFailure = (err, error) => {
77
- error(`Failed: ${err.message}`);
78
- return 1;
79
- };
80
-
81
- export const runInboxList = async (opts = {}, deps = {}) => {
82
- const {
83
- env = process.env, fetchImpl = fetch, log = console.log, error = console.error, sleep = undefined,
84
- } = deps;
85
- try {
86
- // Validated before the request: a typo in --kind must not cost a network
87
- // round trip, and must not read a queue the caller did not ask for.
88
- const kind = normalizeKind(opts.kind);
89
- // Validated here with --kind, for the same reason: a typo must not cost a
90
- // request, and a caller who asked for a window must not silently get another.
91
- const windowMs = parseWindowMs(opts.window);
92
- // Read and check the cursor BEFORE connecting: this is a local-file fact, and
93
- // a misuse of the flags must not cost even the identity request that
94
- // resolves the account.
95
- const sinceRaw = opts.since !== undefined ? opts.since : readCursorFile(opts.cursorFile);
96
- const cursorIn = parseCursor(sinceRaw);
97
- // A resume cursor is valid only for the query that wrote it. Reading a
98
- // kind-scoped cursor with a different filter — including no filter at all —
99
- // would advance past items of other kinds that the earlier read never
100
- // printed, and no later read could reach them. Only this direction is
101
- // refused: a BROAD cursor read by a narrower query loses nothing, because
102
- // that earlier read printed every kind.
103
- if (cursorIn?.kind && cursorIn.kind !== kind) {
104
- throw new InboxRefusal(
105
- `the cursor file was written by a read with --kind ${cursorIn.kind}, and this read is `
106
- + `${kind ? `--kind ${kind}` : 'not filtered by kind'}. A cursor records how far THAT query got, so this `
107
- + `read would skip items of other kinds that one never printed. Keep one cursor file per kind, e.g. `
108
- + `--kind ${cursorIn.kind} --cursor-file <path>.${cursorIn.kind}.`,
109
- );
110
- }
111
- // The window: W behind the cursor's own stamp, not at it. A row stamped before
112
- // the cursor can commit after the read that wrote it, and it is that read's
113
- // `since` — not the row — that made it invisible; a read starting W earlier
114
- // sees it. The ids already delivered inside the window are what keep the
115
- // re-read from printing the old rows again (see lib/inbox.js).
116
- const sinceMs = cursorIn ? cursorIn.ms - windowMs : null;
117
- const since = sinceMs === null ? null : new Date(sinceMs).toISOString();
118
-
119
- const {
120
- baseUrl, request, account,
121
- } = await connect(opts, { env, fetchImpl, sleep });
122
-
123
- const { items, meta } = await fetchQueue(request, { podId: opts.pod, since });
124
- const selected = filterItems(items, {
125
- kind,
126
- sinceMs,
127
- // Everything already delivered inside the window. An item of the same id in
128
- // the response is a re-delivery, not news; an item that merely shares its
129
- // instant with one is news, which is why this dedupes by id and not by time.
130
- seenIds: cursorIn ? cursorIn.ids : [],
131
- });
132
- const cursor = nextCursorFrom(selected, cursorIn, kind, windowMs);
133
-
134
- if (opts.json) {
135
- log(JSON.stringify({
136
- account,
137
- instance: baseUrl,
138
- count: selected.length,
139
- // The bare timestamp: a caller feeding this back through --since loses
140
- // the boundary ids, which degrades to re-printing that millisecond
141
- // rather than to skipping it.
142
- cursor: cursor ? cursor.at : null,
143
- countsByKind: meta?.countsByKind || {},
144
- items: selected,
145
- }, null, 2));
146
- } else {
147
- log(formatAccountLine({ account, baseUrl }));
148
- const counts = countsLine(meta?.countsByKind);
149
- log(`open: ${Number.isInteger(meta?.count) ? meta.count : items.length}${counts ? ` (${counts})` : ''}`);
150
- if (selected.length === 0) {
151
- log(since ? `(nothing new; the window since ${since} was re-read)` : '(queue is empty)');
152
- }
153
- for (const item of selected) log(formatItemLine(item));
154
- log(`cursor: ${cursor ? cursor.at : ''}`);
155
- }
156
- writeCursorFile(opts.cursorFile, cursor);
157
- return 0;
158
- } catch (err) {
159
- if (err instanceof InboxRefusal) return reportRefusal(err, error);
160
- return reportFailure(err, error);
161
- }
162
- };
163
-
164
- export const runInboxAck = async (itemId, opts = {}, deps = {}) => {
165
- const {
166
- env = process.env, fetchImpl = fetch, log = console.log, error = console.error, sleep = undefined,
167
- } = deps;
168
- try {
169
- if (!itemId) throw new InboxRefusal(`an item id is required: ${CLI} inbox ack <attentionItemId>`);
170
- const {
171
- baseUrl, request, account,
172
- } = await connect(opts, { env, fetchImpl, sleep });
173
- log(formatAccountLine({ account, baseUrl }));
174
- const result = await request(`/api/activity/${encodeURIComponent(itemId)}/acknowledge`, {
175
- method: 'POST',
176
- body: {},
177
- });
178
- log(`acked ${itemId}${result?.success === false ? ` — server said: ${result.error || 'not acknowledged'}` : ''}`);
179
- return 0;
180
- } catch (err) {
181
- if (err instanceof InboxRefusal) return reportRefusal(err, error);
182
- if (err?.status === 400) {
183
- error(`Failed: ${err.message}`);
184
- error(`Acknowledge takes the item's attentionItemId — the id printed as \`item=…\` by \`${CLI} inbox list\``
185
- + ` (the unlabelled source id is what \`${CLI} inbox choose\` takes).`);
186
- return 1;
187
- }
188
- return reportFailure(err, error);
189
- }
190
- };
191
-
192
- // `value` is either a 1-based option index (position in the decision's options,
193
- // shown on refusal) or an option label. The server stores whatever string it is sent, so
194
- // the label is resolved here against the open queue and only the label is posted:
195
- // a bare "2" recorded in the ruling would mean nothing to anyone reading it later.
196
- export const resolveDecisionChoice = (options, value) => {
197
- const choices = (Array.isArray(options) ? options : [])
198
- .map((option) => String(option?.label ?? '').trim())
199
- .filter(Boolean);
200
- const listing = choices.map((label, i) => ` ${i + 1}. ${label}`).join('\n');
201
- const raw = String(value).trim();
202
- if (/^\d+$/.test(raw)) {
203
- const index = Number(raw);
204
- const labelled = choices.findIndex((label) => label === raw);
205
- // A digit-only label is also a position in the list. When the two point at
206
- // different options, neither reading is safe to guess, so refuse.
207
- if (labelled !== -1 && labelled !== index - 1) {
208
- throw new InboxRefusal(`"${raw}" is both option ${labelled + 1}'s label and position ${raw}; refusing to guess. Choose by position (1-${choices.length}) or by a label that is not a number:\n${listing}`);
209
- }
210
- if (index < 1 || index > choices.length) {
211
- throw new InboxRefusal(`option ${raw} is out of range; choose 1-${choices.length}:\n${listing}`);
212
- }
213
- return choices[index - 1];
214
- }
215
- const match = choices.find((label) => label.toLocaleLowerCase() === raw.toLocaleLowerCase());
216
- if (!match) throw new InboxRefusal(`"${raw}" is not an option; the options are:\n${listing}`);
217
- return match;
218
- };
219
-
220
- export const runInboxChoose = async (decisionId, value, opts = {}, deps = {}) => {
221
- const {
222
- env = process.env, fetchImpl = fetch, log = console.log, error = console.error, sleep = undefined,
223
- } = deps;
224
- try {
225
- if (!decisionId) throw new InboxRefusal(`a decision id is required: ${CLI} inbox choose <decisionId> <n|label>`);
226
- if (value === undefined) throw new InboxRefusal(`a value is required: ${CLI} inbox choose <decisionId> <n|label>`);
227
- const {
228
- baseUrl, request, account,
229
- } = await connect(opts, { env, fetchImpl, sleep });
230
- log(formatAccountLine({ account, baseUrl }));
231
- // The options live on the open queue, not on the decision route, so the
232
- // choice is resolved against the same queue this account would see in `list`.
233
- const { items } = await fetchQueue(request);
234
- const open = items.find((item) => String(item?.id) === String(decisionId) && item?.kind === 'decision');
235
- if (!open) {
236
- throw new InboxRefusal(`decision ${decisionId} is not open on this account's queue; nothing to choose`);
237
- }
238
- const label = resolveDecisionChoice(open.options, value);
239
- const result = await request(`/api/activity/decisions/${encodeURIComponent(decisionId)}/choose`, {
240
- method: 'POST',
241
- body: { value: label },
242
- });
243
- log(`chose ${decisionId} = ${label}`);
244
- log(`-> ${JSON.stringify(result)}`);
245
- return 0;
246
- } catch (err) {
247
- if (err instanceof InboxRefusal) return reportRefusal(err, error);
248
- return reportFailure(err, error);
249
- }
250
- };
251
-
252
- export const registerInbox = (program) => {
253
- const inbox = program
254
- .command('inbox')
255
- .description("Read and act on ONE account's own attention queue");
256
-
257
- inbox.addHelpText('after', `
258
- The account is read from a token file and from nowhere else:
259
-
260
- $ ${CLI} inbox list --token-file ~/.commonly/bin/connector-ops-token
261
- $ COMMONLY_TOKEN_FILE=~/.commonly/bin/lily-token ${CLI} inbox list
262
-
263
- There is no fallback to the saved login. On a host where \`${CLI} login\` was
264
- run once as somebody else, a fallback would print that person's queue while the
265
- operator believed they were reading their own. Every run therefore names the
266
- account it resolved on its first line.
267
-
268
- With neither --token-file nor COMMONLY_TOKEN_FILE set, the command refuses
269
- before it makes a request.
270
-
271
- Examples:
272
- $ ${CLI} inbox list --json | jq '.items[].title'
273
- $ ${CLI} inbox list --kind decision --pod <podId>
274
- $ ${CLI} inbox list --cursor-file ~/.commonly/inbox/connector-ops.cursor # only what is new
275
- $ ${CLI} inbox list --cursor-file ~/.commonly/inbox/c.cursor --window 300 # widen the re-read to 5m
276
- $ ${CLI} inbox ack <attentionItemId>
277
- $ ${CLI} inbox choose <decisionId> 2 # second option of the card, by position
278
- $ ${CLI} inbox choose <decisionId> "Ship it" # or by its label
279
-
280
- Cost: with no cursor, \`list\` reads the WHOLE queue, one request per 50 items —
281
- 550 open items is 11 requests. /api/activity is limited to 60 a minute **keyed on
282
- the caller's IP**, so every session on one host (and the operator's browser)
283
- shares that budget; a 429 is backed off and retried twice on the server's own
284
- \`Retry-After\`, then reported.
285
-
286
- With \`--cursor-file\`, the read instead asks for \`createdAt >= cursor - W\`
287
- (\`--window\`, default 60s), so a watcher tick is ONE request. W is the margin
288
- over write-commit latency plus the writer's clock offset — a row is stamped before
289
- its write commits, so resuming exactly at the cursor would lose it. Rows already
290
- delivered inside W are dropped by id, which is the re-delivery the window is for.
291
- The counts printed in the header still describe the whole open set; only the rows
292
- inside the window are printed.
293
-
294
- \`ack\` deliberately ships WITHOUT an \`--all\`: a burst of acks shares that same
295
- per-IP budget, so a paced loop is the caller's decision and a bulk
296
- \`ids[]\`-accepting route (one request) is the kernel-side answer.
297
-
298
- Ids: \`list\` prints both ids an item has. \`ack\` takes \`item=…\`
299
- (attentionItemId); \`choose\` takes the item's source id, which for a decision is
300
- the decision id. Agent runtime tokens (cm_agent_…) are out of scope — they get
301
- their attention as runtime events, not from this queue.
302
- `);
303
-
304
- inbox
305
- .command('list')
306
- .description("List the account's open attention queue, newest first")
307
- .option('--token-file <path>', INBOX_TOKEN_HELP)
308
- .option('--since <cursor>', 'ISO-8601 timestamp; only items created after it')
309
- .option('--cursor-file <path>', 'Read the cursor from this file and write the advanced cursor back')
310
- .option('--window <seconds>', 'Re-read this far behind the cursor, dropping ids already delivered (default 60; 0 = exact cursor)')
311
- .option('--kind <kind>', `Only this kind: ${INBOX_KINDS.join('|')}`)
312
- .option('--pod <podId>', 'Only this pod')
313
- .option('--json', 'Emit one JSON object (with the resolved account and the cursor)')
314
- .option('--instance <url>', 'Target Space instance')
315
- .action(async (opts) => { process.exit(await runInboxList(opts)); });
316
-
317
- inbox
318
- .command('ack <itemId>')
319
- .description('Acknowledge an attention item (the item= id from `list`)')
320
- .option('--token-file <path>', INBOX_TOKEN_HELP)
321
- .option('--instance <url>', 'Target Space instance')
322
- .action(async (itemId, opts) => { process.exit(await runInboxAck(itemId, opts)); });
323
-
324
- inbox
325
- .command('choose <decisionId> <value>')
326
- .description('Rule a decision card (the source= id from `list`). <value> is an option number (1-based) or its label')
327
- .option('--token-file <path>', INBOX_TOKEN_HELP)
328
- .option('--instance <url>', 'Target Space instance')
329
- .action(async (decisionId, value, opts) => { process.exit(await runInboxChoose(decisionId, value, opts)); });
330
- };