@ravefox/space 0.2.0 → 0.3.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 (163) hide show
  1. package/LICENSE +21 -203
  2. package/README.md +89 -26
  3. package/bin/space.js +106 -0
  4. package/lib/agents.js +119 -0
  5. package/lib/credentials.js +95 -0
  6. package/lib/daemon.js +294 -0
  7. package/lib/harness.js +166 -0
  8. package/lib/http.js +101 -0
  9. package/lib/login.js +68 -0
  10. package/lib/mcp.js +34 -0
  11. package/lib/runtime.js +241 -0
  12. package/lib/store.js +96 -0
  13. package/lib/tools.js +93 -0
  14. package/package.json +28 -40
  15. package/NOTICE +0 -44
  16. package/assets/webhook-sdk/python/bot.py +0 -63
  17. package/assets/webhook-sdk/python/commonly.py +0 -232
  18. package/node_modules/@commonlyai/seats/README.md +0 -25
  19. package/node_modules/@commonlyai/seats/package.json +0 -22
  20. package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
  21. package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
  22. package/node_modules/@commonlyai/seats/src/contract.js +0 -48
  23. package/node_modules/@commonlyai/seats/src/failures.js +0 -44
  24. package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
  25. package/node_modules/@commonlyai/seats/src/index.js +0 -22
  26. package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
  27. package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
  28. package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
  29. package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
  30. package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
  31. package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
  32. package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
  33. package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
  34. package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
  35. package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
  36. package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
  37. package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
  38. package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
  39. package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
  40. package/skills/commonly/SKILL.md +0 -233
  41. package/src/commands/agent.js +0 -6334
  42. package/src/commands/daemon.js +0 -933
  43. package/src/commands/dev.js +0 -153
  44. package/src/commands/doctor.js +0 -141
  45. package/src/commands/inbox.js +0 -330
  46. package/src/commands/login.js +0 -194
  47. package/src/commands/mcp.js +0 -56
  48. package/src/commands/pod.js +0 -120
  49. package/src/commands/task.js +0 -279
  50. package/src/commands/verify.js +0 -48
  51. package/src/index.js +0 -99
  52. package/src/lib/adapter-login.js +0 -76
  53. package/src/lib/adapters/broker-bridge-entry.js +0 -76
  54. package/src/lib/adapters/claude.js +0 -2207
  55. package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
  56. package/src/lib/adapters/codex.js +0 -1898
  57. package/src/lib/adapters/dsh.js +0 -513
  58. package/src/lib/adapters/goose.js +0 -510
  59. package/src/lib/adapters/hermes.js +0 -18
  60. package/src/lib/adapters/index.js +0 -68
  61. package/src/lib/adapters/opencode.js +0 -1681
  62. package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
  63. package/src/lib/adapters/pi-mcp-client.mjs +0 -617
  64. package/src/lib/adapters/pi-shell-env.mjs +0 -41
  65. package/src/lib/adapters/pi.js +0 -1025
  66. package/src/lib/adapters/qwen.js +0 -523
  67. package/src/lib/adapters/seat-model.js +0 -8
  68. package/src/lib/adapters/stub.js +0 -1
  69. package/src/lib/adapters/tested-version.js +0 -106
  70. package/src/lib/api.js +0 -205
  71. package/src/lib/claim-outcome.js +0 -128
  72. package/src/lib/claimed-work-gate.js +0 -73
  73. package/src/lib/cli-hint.js +0 -33
  74. package/src/lib/commit-attribution.js +0 -391
  75. package/src/lib/config.js +0 -155
  76. package/src/lib/content-refusal.js +0 -94
  77. package/src/lib/credential-file.js +0 -208
  78. package/src/lib/daemon-agent-states.js +0 -95
  79. package/src/lib/daemon-hint.js +0 -18
  80. package/src/lib/daemon-logs.js +0 -43
  81. package/src/lib/daemon-service-windows.js +0 -263
  82. package/src/lib/daemon-service.js +0 -486
  83. package/src/lib/daemon-state.js +0 -84
  84. package/src/lib/daemon-store.js +0 -119
  85. package/src/lib/daemon-supervisor.js +0 -629
  86. package/src/lib/daemon-watch.js +0 -115
  87. package/src/lib/declared-mcp-guard.js +0 -284
  88. package/src/lib/default-environment.js +0 -207
  89. package/src/lib/device-login.js +0 -126
  90. package/src/lib/digest-frame.js +0 -151
  91. package/src/lib/doctor.js +0 -291
  92. package/src/lib/egress-proxy.js +0 -160
  93. package/src/lib/enforcement.js +0 -946
  94. package/src/lib/environment.js +0 -681
  95. package/src/lib/first-output-watchdog.js +0 -191
  96. package/src/lib/git-readonly-server.js +0 -138
  97. package/src/lib/git-readonly.js +0 -463
  98. package/src/lib/git-remote-commonly.mjs +0 -100
  99. package/src/lib/grant-broker-guard.js +0 -345
  100. package/src/lib/hooks-config.js +0 -238
  101. package/src/lib/host-gh-broker.js +0 -152
  102. package/src/lib/host-gh-shim.mjs +0 -98
  103. package/src/lib/inbox.js +0 -483
  104. package/src/lib/instance-proxy.js +0 -76
  105. package/src/lib/machine-host.js +0 -29
  106. package/src/lib/mcp-credential-delivery.js +0 -121
  107. package/src/lib/mcp-first-run.js +0 -50
  108. package/src/lib/mcp-home.js +0 -644
  109. package/src/lib/mcp-ready.js +0 -68
  110. package/src/lib/mcp-server-version.js +0 -123
  111. package/src/lib/mcp-token.js +0 -36
  112. package/src/lib/mcp-warm-child.mjs +0 -412
  113. package/src/lib/memory-bridge.js +0 -191
  114. package/src/lib/memory-import.js +0 -156
  115. package/src/lib/missing-harness.js +0 -31
  116. package/src/lib/model-unavailable.js +0 -154
  117. package/src/lib/native-runtime-inventory.js +0 -40
  118. package/src/lib/outsider-grant-mcp.js +0 -82
  119. package/src/lib/own-key-launch.js +0 -42
  120. package/src/lib/pod-catch-up.js +0 -55
  121. package/src/lib/pod-focus.js +0 -224
  122. package/src/lib/pod-tail.js +0 -68
  123. package/src/lib/poll-liveness.js +0 -103
  124. package/src/lib/poll-retry.js +0 -81
  125. package/src/lib/poller.js +0 -200
  126. package/src/lib/provider-proxy.js +0 -42
  127. package/src/lib/public-seat-mcp-flag.js +0 -36
  128. package/src/lib/public-tool-error.js +0 -60
  129. package/src/lib/public-withheld-tools.js +0 -48
  130. package/src/lib/reply-receipt.js +0 -55
  131. package/src/lib/resolve-on-path.js +0 -22
  132. package/src/lib/runtime-inventory.js +0 -56
  133. package/src/lib/runtime-observation.js +0 -37
  134. package/src/lib/sandbox/bwrap.js +0 -275
  135. package/src/lib/sandbox/codex-perimeter.js +0 -16
  136. package/src/lib/sandbox/host-check.js +0 -268
  137. package/src/lib/sandbox/mode.js +0 -49
  138. package/src/lib/sandbox/opencode-loopback.mjs +0 -68
  139. package/src/lib/sandbox/seatbelt.js +0 -684
  140. package/src/lib/sandbox/turn-dir.js +0 -56
  141. package/src/lib/sandbox/windows-unsandboxed.js +0 -183
  142. package/src/lib/sandbox/workspace-git-config.js +0 -243
  143. package/src/lib/sandbox/workspace-pin.js +0 -158
  144. package/src/lib/seat-runtime-controls.js +0 -61
  145. package/src/lib/seat-stop.js +0 -97
  146. package/src/lib/server-skills.js +0 -32
  147. package/src/lib/session-recycle.js +0 -219
  148. package/src/lib/session-store.js +0 -215
  149. package/src/lib/skill-prompt.js +0 -74
  150. package/src/lib/skills-import.js +0 -113
  151. package/src/lib/spawn-credential.js +0 -402
  152. package/src/lib/spawn-retry.js +0 -311
  153. package/src/lib/stable-install.js +0 -176
  154. package/src/lib/supervised-turn.js +0 -352
  155. package/src/lib/task.js +0 -235
  156. package/src/lib/turn-failure-notice.js +0 -177
  157. package/src/lib/unconfined-seat-guard.js +0 -94
  158. package/src/lib/update-check.js +0 -192
  159. package/src/lib/upstream-refusal.js +0 -1
  160. package/src/lib/webhook-server.js +0 -109
  161. package/src/lib/windows-command.js +0 -393
  162. package/src/lib/workspace-git.js +0 -87
  163. 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
- };