@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.
- package/LICENSE +21 -203
- package/README.md +89 -26
- package/bin/space.js +106 -0
- package/lib/agents.js +119 -0
- package/lib/credentials.js +95 -0
- package/lib/daemon.js +294 -0
- package/lib/harness.js +166 -0
- package/lib/http.js +101 -0
- package/lib/login.js +68 -0
- package/lib/mcp.js +34 -0
- package/lib/runtime.js +241 -0
- package/lib/store.js +96 -0
- package/lib/tools.js +93 -0
- package/package.json +28 -40
- package/NOTICE +0 -44
- package/assets/webhook-sdk/python/bot.py +0 -63
- package/assets/webhook-sdk/python/commonly.py +0 -232
- package/node_modules/@commonlyai/seats/README.md +0 -25
- package/node_modules/@commonlyai/seats/package.json +0 -22
- package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
- package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
- package/node_modules/@commonlyai/seats/src/contract.js +0 -48
- package/node_modules/@commonlyai/seats/src/failures.js +0 -44
- package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
- package/node_modules/@commonlyai/seats/src/index.js +0 -22
- package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
- package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
- package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
- package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
- package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
- package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
- package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
- package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
- package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
- package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
- package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
- package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
- package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
- package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
- package/skills/commonly/SKILL.md +0 -233
- package/src/commands/agent.js +0 -6334
- package/src/commands/daemon.js +0 -933
- package/src/commands/dev.js +0 -153
- package/src/commands/doctor.js +0 -141
- package/src/commands/inbox.js +0 -330
- package/src/commands/login.js +0 -194
- package/src/commands/mcp.js +0 -56
- package/src/commands/pod.js +0 -120
- package/src/commands/task.js +0 -279
- package/src/commands/verify.js +0 -48
- package/src/index.js +0 -99
- package/src/lib/adapter-login.js +0 -76
- package/src/lib/adapters/broker-bridge-entry.js +0 -76
- package/src/lib/adapters/claude.js +0 -2207
- package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
- package/src/lib/adapters/codex.js +0 -1898
- package/src/lib/adapters/dsh.js +0 -513
- package/src/lib/adapters/goose.js +0 -510
- package/src/lib/adapters/hermes.js +0 -18
- package/src/lib/adapters/index.js +0 -68
- package/src/lib/adapters/opencode.js +0 -1681
- package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
- package/src/lib/adapters/pi-mcp-client.mjs +0 -617
- package/src/lib/adapters/pi-shell-env.mjs +0 -41
- package/src/lib/adapters/pi.js +0 -1025
- package/src/lib/adapters/qwen.js +0 -523
- package/src/lib/adapters/seat-model.js +0 -8
- package/src/lib/adapters/stub.js +0 -1
- package/src/lib/adapters/tested-version.js +0 -106
- package/src/lib/api.js +0 -205
- package/src/lib/claim-outcome.js +0 -128
- package/src/lib/claimed-work-gate.js +0 -73
- package/src/lib/cli-hint.js +0 -33
- package/src/lib/commit-attribution.js +0 -391
- package/src/lib/config.js +0 -155
- package/src/lib/content-refusal.js +0 -94
- package/src/lib/credential-file.js +0 -208
- package/src/lib/daemon-agent-states.js +0 -95
- package/src/lib/daemon-hint.js +0 -18
- package/src/lib/daemon-logs.js +0 -43
- package/src/lib/daemon-service-windows.js +0 -263
- package/src/lib/daemon-service.js +0 -486
- package/src/lib/daemon-state.js +0 -84
- package/src/lib/daemon-store.js +0 -119
- package/src/lib/daemon-supervisor.js +0 -629
- package/src/lib/daemon-watch.js +0 -115
- package/src/lib/declared-mcp-guard.js +0 -284
- package/src/lib/default-environment.js +0 -207
- package/src/lib/device-login.js +0 -126
- package/src/lib/digest-frame.js +0 -151
- package/src/lib/doctor.js +0 -291
- package/src/lib/egress-proxy.js +0 -160
- package/src/lib/enforcement.js +0 -946
- package/src/lib/environment.js +0 -681
- package/src/lib/first-output-watchdog.js +0 -191
- package/src/lib/git-readonly-server.js +0 -138
- package/src/lib/git-readonly.js +0 -463
- package/src/lib/git-remote-commonly.mjs +0 -100
- package/src/lib/grant-broker-guard.js +0 -345
- package/src/lib/hooks-config.js +0 -238
- package/src/lib/host-gh-broker.js +0 -152
- package/src/lib/host-gh-shim.mjs +0 -98
- package/src/lib/inbox.js +0 -483
- package/src/lib/instance-proxy.js +0 -76
- package/src/lib/machine-host.js +0 -29
- package/src/lib/mcp-credential-delivery.js +0 -121
- package/src/lib/mcp-first-run.js +0 -50
- package/src/lib/mcp-home.js +0 -644
- package/src/lib/mcp-ready.js +0 -68
- package/src/lib/mcp-server-version.js +0 -123
- package/src/lib/mcp-token.js +0 -36
- package/src/lib/mcp-warm-child.mjs +0 -412
- package/src/lib/memory-bridge.js +0 -191
- package/src/lib/memory-import.js +0 -156
- package/src/lib/missing-harness.js +0 -31
- package/src/lib/model-unavailable.js +0 -154
- package/src/lib/native-runtime-inventory.js +0 -40
- package/src/lib/outsider-grant-mcp.js +0 -82
- package/src/lib/own-key-launch.js +0 -42
- package/src/lib/pod-catch-up.js +0 -55
- package/src/lib/pod-focus.js +0 -224
- package/src/lib/pod-tail.js +0 -68
- package/src/lib/poll-liveness.js +0 -103
- package/src/lib/poll-retry.js +0 -81
- package/src/lib/poller.js +0 -200
- package/src/lib/provider-proxy.js +0 -42
- package/src/lib/public-seat-mcp-flag.js +0 -36
- package/src/lib/public-tool-error.js +0 -60
- package/src/lib/public-withheld-tools.js +0 -48
- package/src/lib/reply-receipt.js +0 -55
- package/src/lib/resolve-on-path.js +0 -22
- package/src/lib/runtime-inventory.js +0 -56
- package/src/lib/runtime-observation.js +0 -37
- package/src/lib/sandbox/bwrap.js +0 -275
- package/src/lib/sandbox/codex-perimeter.js +0 -16
- package/src/lib/sandbox/host-check.js +0 -268
- package/src/lib/sandbox/mode.js +0 -49
- package/src/lib/sandbox/opencode-loopback.mjs +0 -68
- package/src/lib/sandbox/seatbelt.js +0 -684
- package/src/lib/sandbox/turn-dir.js +0 -56
- package/src/lib/sandbox/windows-unsandboxed.js +0 -183
- package/src/lib/sandbox/workspace-git-config.js +0 -243
- package/src/lib/sandbox/workspace-pin.js +0 -158
- package/src/lib/seat-runtime-controls.js +0 -61
- package/src/lib/seat-stop.js +0 -97
- package/src/lib/server-skills.js +0 -32
- package/src/lib/session-recycle.js +0 -219
- package/src/lib/session-store.js +0 -215
- package/src/lib/skill-prompt.js +0 -74
- package/src/lib/skills-import.js +0 -113
- package/src/lib/spawn-credential.js +0 -402
- package/src/lib/spawn-retry.js +0 -311
- package/src/lib/stable-install.js +0 -176
- package/src/lib/supervised-turn.js +0 -352
- package/src/lib/task.js +0 -235
- package/src/lib/turn-failure-notice.js +0 -177
- package/src/lib/unconfined-seat-guard.js +0 -94
- package/src/lib/update-check.js +0 -192
- package/src/lib/upstream-refusal.js +0 -1
- package/src/lib/webhook-server.js +0 -109
- package/src/lib/windows-command.js +0 -393
- package/src/lib/workspace-git.js +0 -87
- package/src/lib/workspace-trust.js +0 -162
package/src/commands/dev.js
DELETED
|
@@ -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
|
-
};
|
package/src/commands/doctor.js
DELETED
|
@@ -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
|
-
};
|
package/src/commands/inbox.js
DELETED
|
@@ -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
|
-
};
|