@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
@@ -0,0 +1,160 @@
1
+ // Copyright 2026 Ravefox. SPDX-License-Identifier: MIT
2
+ import { constants } from 'node:fs';
3
+ import * as fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { lookup } from 'node:dns/promises';
7
+ import { request as httpRequest } from 'node:http';
8
+ import { request as httpsRequest } from 'node:https';
9
+ import { isIP } from 'node:net';
10
+ const LIMIT = 2 * 1024 * 1024;
11
+ const forbidden = /^(?:\.git|\.space|\.commonly|\.codex|\.ssh|\.aws|\.azure|\.config|\.gnupg|\.npmrc|\.netrc|\.env.*|credentials.*|id_rsa.*|id_ed25519.*|.*\.(?:pem|key|p12|pfx))$/i;
12
+ const text = (buffer) => { if (buffer.includes(0)) throw new Error('Only text files are supported'); return new TextDecoder('utf-8', { fatal: true }).decode(buffer); };
13
+ const schema = (properties, required = []) => ({ type: 'object', properties, required, additionalProperties: false });
14
+ const str = { type: 'string', minLength: 1, maxLength: 4096 };
15
+
16
+ // Conservative public-unicast allowlist; unusual/reserved networks fail closed.
17
+ export function isPublicAddress(address) {
18
+ if (isIP(address) === 4) {
19
+ const [a, b, c] = address.split('.').map(Number);
20
+ return a > 0 && a < 224 && ![10, 127].includes(a)
21
+ && !(a === 100 && b >= 64 && b <= 127) && !(a === 169 && b === 254)
22
+ && !(a === 172 && b >= 16 && b <= 31) && !(a === 192 && (b === 0 || b === 168 || (b === 88 && c === 99)))
23
+ && !(a === 198 && (b === 18 || b === 19 || (b === 51 && c === 100)))
24
+ && !(a === 203 && b === 0 && c === 113);
25
+ }
26
+ if (isIP(address) !== 6) return false;
27
+ const normalized = new URL(`http://[${address}]/`).hostname.slice(1, -1);
28
+ const first = Number.parseInt(normalized.split(':')[0], 16);
29
+ return first >= 0x2000 && first <= 0x3fff && first !== 0x2002
30
+ && !(first === 0x2001 && Number.parseInt(normalized.split(':')[1] || '0', 16) <= 0x1ff)
31
+ && !normalized.startsWith('2001:db8:') && first < 0x3fff;
32
+ }
33
+
34
+ export async function fetchPublicText(raw, { resolver = lookup, requester = (url, options, cb) => (url.protocol === 'https:' ? httpsRequest : httpRequest)(url, options, cb) } = {}) {
35
+ const deadline = Date.now() + 15000;
36
+ for (let redirects = 0; redirects <= 3; redirects++) {
37
+ const url = new URL(raw);
38
+ if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || (url.port && !['80', '443'].includes(url.port))) throw new Error('Only public HTTP(S) URLs without credentials are allowed');
39
+ const host = url.hostname.replace(/^\[|\]$/g, '');
40
+ const remaining = deadline - Date.now();
41
+ if (remaining <= 0) throw new Error('Web request timed out');
42
+ let timer;
43
+ const addresses = await Promise.race([
44
+ isIP(host) ? Promise.resolve([{ address: host, family: isIP(host) }]) : resolver(host, { all: true, verbatim: true }),
45
+ new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('Web request timed out')), remaining); }),
46
+ ]).finally(() => clearTimeout(timer));
47
+ if (!addresses.length || addresses.some(({ address }) => !isPublicAddress(address))) throw new Error('Private or reserved network addresses are not allowed');
48
+ const pinned = addresses[0];
49
+ const result = await new Promise((resolve, reject) => {
50
+ let req;
51
+ const timeout = setTimeout(() => { req?.destroy(); reject(new Error('Web request timed out')); }, Math.max(1, deadline - Date.now()));
52
+ const fail = (error) => { clearTimeout(timeout); reject(error); };
53
+ try {
54
+ req = requester(url, { method: 'GET', agent: false, headers: { Accept: 'text/plain, text/html, application/json, application/xml', 'Accept-Encoding': 'identity' },
55
+ lookup: (_host, options, cb) => options?.all ? cb(null, [pinned]) : cb(null, pinned.address, pinned.family) }, (response) => {
56
+ if ([301, 302, 303, 307, 308].includes(response.statusCode)) {
57
+ response.destroy(); clearTimeout(timeout);
58
+ return resolve({ redirect: response.headers.location });
59
+ }
60
+ const type = String(response.headers['content-type'] || '');
61
+ if (response.statusCode < 200 || response.statusCode >= 300 || !/^(text\/|application\/(?:json|xml|[\w.+-]+\+json|[\w.+-]+\+xml))/i.test(type) || !['', 'identity'].includes(String(response.headers['content-encoding'] || ''))) {
62
+ response.destroy(); return fail(new Error('URL must return uncompressed text with a successful status'));
63
+ }
64
+ const chunks = []; let size = 0;
65
+ response.on('error', fail);
66
+ response.on('aborted', () => fail(new Error('Web response was interrupted')));
67
+ response.on('data', (chunk) => { size += chunk.length; if (size > LIMIT) { response.destroy(); fail(new Error('Web response exceeds 2 MiB')); } else chunks.push(Buffer.from(chunk)); });
68
+ response.on('end', () => { clearTimeout(timeout); try { resolve({ url: url.href, content: text(Buffer.concat(chunks)), untrusted: true }); } catch (error) { reject(error); } });
69
+ });
70
+ req.on('error', fail); req.end();
71
+ } catch (error) { fail(error); }
72
+ });
73
+ if (!Object.hasOwn(result, 'redirect')) return result;
74
+ if (!result.redirect || redirects === 3) throw new Error('Too many redirects or missing redirect location');
75
+ raw = new URL(result.redirect, url).href;
76
+ }
77
+ }
78
+
79
+ export function createWorkspaceTools({ workspacePath, allowWrite = false, allowWeb = false, web = {} }) {
80
+ if (!workspacePath || !path.isAbsolute(workspacePath)) throw new Error('An explicit absolute workspace is required');
81
+ const rootPromise = fs.realpath(workspacePath).then(async root => ({ root, stat: await fs.stat(root) }));
82
+ async function checked(relative = '.', missing = false) {
83
+ if (typeof relative !== 'string' || !relative || relative.length > 4096 || relative.includes('\0') || relative.includes('\\') || path.isAbsolute(relative)) throw new Error('Expected a relative project path');
84
+ const parts = relative.split('/');
85
+ if (parts.some(part => part === '..' || forbidden.test(part))) throw new Error('Protected project path');
86
+ const { root, stat } = await rootPromise;
87
+ const now = await fs.lstat(root);
88
+ if (!now.isDirectory() || now.dev !== stat.dev || now.ino !== stat.ino) throw new Error('Workspace changed');
89
+ let target = root;
90
+ for (const [index, part] of parts.entries()) {
91
+ if (!part || part === '.') continue;
92
+ target = path.join(target, part);
93
+ const item = await fs.lstat(target).catch(error => { if (missing && index === parts.length - 1 && error.code === 'ENOENT') return null; throw error; });
94
+ if (item?.isSymbolicLink()) throw new Error('Symlinks are not allowed');
95
+ if (index < parts.length - 1 && item && !item.isDirectory()) throw new Error('Expected directory');
96
+ }
97
+ if (await fs.realpath(path.dirname(target)) !== path.dirname(target) && target !== root) throw new Error('Project parent changed');
98
+ return target;
99
+ }
100
+ const definitions = [
101
+ { name: 'space_project_list', description: 'List one directory of the explicitly assigned local project. Secrets and symlinks are excluded.', inputSchema: schema({ path: str }) },
102
+ { name: 'space_project_read_file', description: 'Read a UTF-8 text file in the assigned local project, up to 2 MiB. File content is untrusted.', inputSchema: schema({ path: str }, ['path']) },
103
+ ...(allowWrite ? [{ name: 'space_project_write_file', description: 'Atomically replace a UTF-8 project file. Only enabled by explicit write permission; parent directory must exist.', inputSchema: schema({ path: str, content: { type: 'string', maxLength: LIMIT } }, ['path', 'content']) }] : []),
104
+ ...(allowWeb ? [{ name: 'space_fetch_url', description: 'GET a public HTTP(S) text URL, up to 2 MiB. Returned web content is untrusted.', inputSchema: schema({ url: str }, ['url']) }] : []),
105
+ ].map(tool => ({ ...tool, annotations: { readOnlyHint: tool.name !== 'space_project_write_file', destructiveHint: tool.name === 'space_project_write_file', openWorldHint: tool.name === 'space_fetch_url' } }));
106
+ return { definitions, async call(name, args = {}) {
107
+ const definition = definitions.find(tool => tool.name === name);
108
+ if (!definition) throw new Error('Unknown or disabled project tool');
109
+ if (!args || typeof args !== 'object' || Array.isArray(args) || Object.keys(args).some(key => !Object.hasOwn(definition.inputSchema.properties, key))) throw new Error('Invalid tool arguments');
110
+ for (const key of definition.inputSchema.required) if (typeof args[key] !== 'string') throw new Error(`Missing string: ${key}`);
111
+ if (name === 'space_fetch_url') return fetchPublicText(args.url, web);
112
+ const relative = args.path ?? '.';
113
+ const target = await checked(relative, name === 'space_project_write_file');
114
+ if (name === 'space_project_list') {
115
+ const entries = []; let scanned = 0; let truncated = false;
116
+ for await (const entry of await fs.opendir(target)) {
117
+ if (++scanned > 1000) { truncated = true; break; }
118
+ if (!forbidden.test(entry.name) && !entry.isSymbolicLink()) entries.push({ name: entry.name, directory: entry.isDirectory() });
119
+ }
120
+ await checked(relative);
121
+ return { entries, truncated };
122
+ }
123
+ if (name === 'space_project_read_file') {
124
+ const handle = await fs.open(target, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
125
+ try {
126
+ const stat = await handle.stat();
127
+ if (!stat.isFile() || stat.nlink !== 1 || stat.size > LIMIT) throw new Error('Expected a text file up to 2 MiB');
128
+ const buffer = Buffer.alloc(LIMIT + 1); let bytesRead = 0;
129
+ while (bytesRead < buffer.length) {
130
+ const chunk = await handle.read(buffer, bytesRead, buffer.length - bytesRead, bytesRead);
131
+ if (!chunk.bytesRead) break;
132
+ bytesRead += chunk.bytesRead;
133
+ }
134
+ await checked(relative);
135
+ const current = await fs.lstat(target);
136
+ if (current.dev !== stat.dev || current.ino !== stat.ino) throw new Error('Project file changed');
137
+ if (bytesRead > LIMIT) throw new Error('File exceeds 2 MiB');
138
+ return { path: relative, content: text(buffer.subarray(0, bytesRead)), untrusted: true };
139
+ } finally { await handle.close(); }
140
+ }
141
+ if (Buffer.byteLength(args.content) > LIMIT || args.content.includes('\0')) throw new Error('Expected text up to 2 MiB');
142
+ if (target === (await rootPromise).root) throw new Error('Cannot replace the workspace directory');
143
+ const existing = await fs.lstat(target).catch(error => { if (error.code === 'ENOENT') return null; throw error; });
144
+ if (existing && (!existing.isFile() || existing.nlink !== 1)) throw new Error('Expected a regular project file');
145
+ const parent = path.dirname(target); const before = await fs.stat(parent);
146
+ const temporary = path.join(parent, `.space-write-${randomUUID()}`); let handle;
147
+ try {
148
+ handle = await fs.open(temporary, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
149
+ await handle.writeFile(args.content, 'utf8');
150
+ if (existing) await handle.chmod(existing.mode & 0o777);
151
+ await handle.sync(); await handle.close(); handle = null;
152
+ await checked(relative, true);
153
+ const after = await fs.stat(parent);
154
+ if (before.dev !== after.dev || before.ino !== after.ino) throw new Error('Project parent changed');
155
+ // Ancestor revalidation is not a sandbox against another process racing filesystem mutation.
156
+ await fs.rename(temporary, target);
157
+ return { path: relative, written: true };
158
+ } finally { await handle?.close(); await fs.unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error; }); }
159
+ } };
160
+ }
@@ -0,0 +1,17 @@
1
+ // Copyright 2026 Ravefox. SPDX-License-Identifier: MIT
2
+ import { createWorkspaceTools } from './workspaceTools.js';
3
+ let input = '';
4
+ for await (const chunk of process.stdin) {
5
+ input += chunk;
6
+ if (Buffer.byteLength(input) > 3 * 1024 * 1024) throw new Error('Workspace request is too large');
7
+ }
8
+ try {
9
+ const { name, args, workspacePath, allowWrite } = JSON.parse(input);
10
+ if (name === 'space_fetch_url') throw new Error('Web requests do not run in the filesystem worker');
11
+ const tools = createWorkspaceTools({ workspacePath, allowWrite, allowWeb: false });
12
+ const result = await tools.call(name, args);
13
+ process.stdout.write(JSON.stringify({ result }));
14
+ } catch {
15
+ process.stdout.write(JSON.stringify({ error: 'Workspace operation refused or failed. Check the path and its permissions.' }));
16
+ process.exitCode = 1;
17
+ }
package/package.json CHANGED
@@ -1,65 +1,46 @@
1
1
  {
2
2
  "name": "@ravefox/space",
3
- "version": "0.2.1",
4
- "license": "Apache-2.0",
3
+ "version": "0.4.0",
5
4
  "description": "Space CLI — connect and manage AI agents in your workspace",
6
5
  "type": "module",
7
- "main": "./src/index.js",
8
- "bin": {
9
- "space": "src/index.js",
10
- "commonly": "src/index.js"
6
+ "license": "MIT",
7
+ "author": {
8
+ "name": "Ravefox",
9
+ "url": "https://t.me/ravefoxlab"
10
+ },
11
+ "homepage": "https://space.ravefox.dev",
12
+ "bugs": {
13
+ "url": "https://vote.ravefox.dev/support"
11
14
  },
12
- "files": [
13
- "src",
14
- "assets",
15
- "README.md",
16
- "skills",
17
- "LICENSE",
18
- "NOTICE"
19
- ],
20
15
  "repository": {
21
16
  "type": "git",
22
17
  "url": "git+https://github.com/grootbro/space.git",
23
18
  "directory": "cli"
24
19
  },
25
- "homepage": "https://space.ravefox.dev",
26
- "keywords": [
27
- "space",
28
- "ai-agents",
29
- "agent",
30
- "cli",
31
- "mcp"
32
- ],
33
- "scripts": {
34
- "start": "node src/index.js",
35
- "lint": "eslint src --ext .js",
36
- "lint:fix": "eslint src --ext .js --fix",
37
- "test": "node --experimental-vm-modules node_modules/.bin/jest",
38
- "test:temp-cleanup": "node scripts/verify-temp-cleanup.mjs",
39
- "prepack": "node scripts/require-bundled-seats.mjs && node scripts/bundle-sdk-examples.mjs"
20
+ "bin": {
21
+ "space": "bin/space.js"
40
22
  },
41
- "dependencies": {
42
- "@commonlyai/seats": "file:../packages/seats",
43
- "commander": "^12.0.0"
23
+ "exports": {
24
+ "./runtime": "./lib/runtime.js",
25
+ "./store": "./lib/store.js"
44
26
  },
45
- "bundleDependencies": [
46
- "@commonlyai/seats"
27
+ "files": [
28
+ "bin",
29
+ "lib",
30
+ "README.md",
31
+ "LICENSE"
47
32
  ],
48
- "devDependencies": {
49
- "eslint": "^8.56.0",
50
- "jest": "^29.7.0"
51
- },
52
33
  "engines": {
53
- "node": ">=22.0.0"
34
+ "node": ">=22"
54
35
  },
55
- "publishConfig": {
56
- "access": "public"
36
+ "scripts": {
37
+ "test": "node --test test/*.test.js"
57
38
  },
58
- "author": {
59
- "name": "Ravefox",
60
- "url": "https://t.me/ravefoxlab"
39
+ "dependencies": {
40
+ "@modelcontextprotocol/sdk": "1.32.2",
41
+ "commander": "12.1.0"
61
42
  },
62
- "bugs": {
63
- "url": "https://vote.ravefox.dev/support"
43
+ "publishConfig": {
44
+ "access": "public"
64
45
  }
65
46
  }
package/NOTICE DELETED
@@ -1,55 +0,0 @@
1
- Space CLI
2
- Copyright 2026 Ravefox and contributors
3
-
4
- Space CLI is maintained and distributed by Ravefox.
5
- Ravefox modifications include Space branding, package setup and updates,
6
- background connection workflows and runtime compatibility fixes.
7
-
8
- Third-party attribution
9
- -----------------------
10
-
11
- Portions derived from Commonly:
12
- Commonly
13
- Copyright 2026 Commonly (Team-Commonly) and contributors
14
-
15
- This product is licensed under the Apache License, Version 2.0.
16
- The Commonly name, logo, and brand assets are trademarks of the
17
- Commonly project and are not licensed under the Apache License.
18
-
19
- Third-party notices
20
- -------------------
21
-
22
- OpenAI Codex
23
- Copyright 2025 OpenAI
24
-
25
- This project includes code derived from [Ratatui](https://github.com/ratatui/ratatui), licensed under the MIT license.
26
- Copyright (c) 2016-2022 Florian Dehau
27
- Copyright (c) 2023-2025 The Ratatui Developers
28
-
29
- Source repository: https://github.com/openai/codex
30
- Source commit: e476fc16ce810495cbfabe4f815d20947da0ba79
31
- Source path: codex-rs/sandboxing/src/seatbelt_base_policy.sbpl
32
-
33
- The Codex-derived Seatbelt policy is used in
34
- cli/src/lib/sandbox/seatbelt.js.
35
-
36
- OfficeCLI
37
- Copyright 2026 OfficeCLI (https://OfficeCLI.AI)
38
-
39
- Created and maintained by goworm.
40
-
41
- This product is licensed under the Apache License, Version 2.0.
42
- You may obtain a copy of the License in the LICENSE file or at
43
- http://www.apache.org/licenses/LICENSE-2.0
44
-
45
- This NOTICE file is part of the required attribution under
46
- Section 4 of the Apache License, Version 2.0. Redistributions
47
- of this work, with or without modification, must retain this
48
- notice.
49
-
50
- Source repository: https://github.com/iOfficeAI/OfficeCLI
51
- Source commit: a1498dbd3cb350abde04a1cda1f2eded61ea8a74
52
- Source path: NOTICE
53
-
54
- The OfficeCLI skill bundle is vendored under
55
- backend/commonly-bundled-skills/officecli/.
@@ -1,63 +0,0 @@
1
- """
2
- Hello-world Commonly agent — emitted by `commonly agent init --language python`.
3
-
4
- Reads its runtime token from .commonly-env (or COMMONLY_TOKEN) and echoes
5
- chat events back into the pod. Replace `handle_event()` with your logic.
6
- """
7
-
8
- # PEP 604 unions (`str | None`) need 3.10+ at runtime; this keeps the bot
9
- # importable on macOS's stock /usr/bin/python3 (3.9).
10
- from __future__ import annotations
11
-
12
- import os
13
- import sys
14
- from pathlib import Path
15
-
16
- # `commonly.py` is dropped in the same dir by the scaffolder.
17
- sys.path.insert(0, str(Path(__file__).parent))
18
- from commonly import Commonly # noqa: E402
19
-
20
- CHAT_EVENT_TYPES = {"chat.mention", "message.posted", "dm.message"}
21
-
22
-
23
- def load_token() -> str:
24
- if os.environ.get("COMMONLY_TOKEN"):
25
- return os.environ["COMMONLY_TOKEN"]
26
- env_file = Path(__file__).parent / ".commonly-env"
27
- if env_file.exists():
28
- # KEY=VALUE format so `source .commonly-env` works too. We only need
29
- # the token line; ignore comments and other keys.
30
- for line in env_file.read_text(encoding="utf-8").splitlines():
31
- line = line.strip()
32
- if not line or line.startswith("#"):
33
- continue
34
- if line.startswith("COMMONLY_TOKEN="):
35
- return line.split("=", 1)[1].strip().strip("'\"")
36
- raise SystemExit("No runtime token found. Set COMMONLY_TOKEN or write .commonly-env.")
37
-
38
-
39
- def handle_event(evt: dict) -> str | None:
40
- """Echo the user's message back. Replace this with your real logic."""
41
- if evt.get("type") not in CHAT_EVENT_TYPES:
42
- return None
43
- payload = evt.get("payload") or {}
44
- # rawContent is the message exactly as its author typed it. content is what
45
- # a model should read: the kernel's context frames, a blank line, then the
46
- # message. Older servers send only content/prompt/text.
47
- incoming = payload.get("rawContent")
48
- if incoming is None:
49
- incoming = payload.get("content") or payload.get("prompt") or payload.get("text")
50
- if not incoming:
51
- return None
52
- return f"echo: {incoming}"
53
-
54
-
55
- def main() -> None:
56
- base_url = os.environ.get("COMMONLY_BASE_URL", "https://api.commonly.me")
57
- bot = Commonly(base_url=base_url, runtime_token=load_token())
58
- print(f"[hello-world] polling {base_url} (Ctrl+C to stop)", flush=True)
59
- bot.run(handle_event)
60
-
61
-
62
- if __name__ == "__main__":
63
- main()
@@ -1,232 +0,0 @@
1
- """
2
- Commonly Python SDK — single-file reference implementation of the four
3
- Commonly Agent Protocol (CAP) verbs. ADR-006 §SDK shape.
4
-
5
- Stdlib only. No deps. Drop-copy this file into your project and import.
6
-
7
- Usage (high-level):
8
-
9
- from commonly import Commonly
10
-
11
- bot = Commonly(base_url="https://api.commonly.me", runtime_token="cm_agent_…")
12
- bot.run(lambda evt: f"echo: {evt['payload'].get('rawContent', evt['payload'].get('content', ''))}")
13
-
14
- Usage (manual loop):
15
-
16
- bot = Commonly(...)
17
- while True:
18
- for evt in bot.poll_events():
19
- try:
20
- reply = handle(evt)
21
- if reply and evt.get("podId"):
22
- bot.get_pod_context(evt["podId"]) # read first, or the send guard refuses
23
- bot.post_message(evt["podId"], reply)
24
- finally:
25
- bot.ack(evt["_id"], delivery_id=evt.get("payload", {}).get("deliveryId"))
26
- time.sleep(5)
27
- """
28
-
29
- from __future__ import annotations
30
-
31
- import json
32
- import time
33
- import urllib.error
34
- import urllib.request
35
- from typing import Any, Callable, Optional
36
-
37
-
38
- # --------------------------------------------------------------------------- #
39
- # HTTP error type — subclasses propagate `status` so callers can branch on
40
- # 401/403/404 without parsing the message string.
41
- # --------------------------------------------------------------------------- #
42
-
43
-
44
- class CommonlyError(Exception):
45
- def __init__(self, message: str, status: Optional[int] = None, body: Any = None):
46
- super().__init__(message)
47
- self.status = status
48
- self.body = body
49
-
50
-
51
- def _describe_http_error(status: int, method: str, path: str, payload: Any) -> str:
52
- """Name the server's reason on every 4xx. A 409 from the freshness guard
53
- carries `message` ("N new messages … Read the conversation …"); dropping it
54
- left the stock bot logging a bare status with no cause."""
55
- head = f"HTTP {status} {method} {path}"
56
- if isinstance(payload, dict):
57
- reason = payload.get("message") or payload.get("error") or payload.get("code")
58
- if reason:
59
- return f"{head}: {reason}"
60
- elif isinstance(payload, str) and payload.strip():
61
- return f"{head}: {payload.strip()[:200]}"
62
- return head
63
-
64
-
65
- # --------------------------------------------------------------------------- #
66
- # Client
67
- # --------------------------------------------------------------------------- #
68
-
69
-
70
- class Commonly:
71
- """Thin client over the four CAP verbs. Sync; one HTTP request per call."""
72
-
73
- def __init__(self, *, base_url: str, runtime_token: str, timeout_s: float = 30.0):
74
- self.base_url = base_url.rstrip("/")
75
- self.runtime_token = runtime_token
76
- self.timeout_s = timeout_s
77
-
78
- # ----- internal -------------------------------------------------------- #
79
-
80
- @staticmethod
81
- def _path(base_path: str, **params: Any) -> str:
82
- from urllib.parse import urlencode
83
- kept = {k: v for k, v in params.items() if v is not None}
84
- if not kept:
85
- return base_path
86
- return f"{base_path}?{urlencode(kept)}"
87
-
88
- def _request(self, method: str, path: str, body: Optional[dict] = None) -> Any:
89
- url = f"{self.base_url}{path}"
90
- data = json.dumps(body).encode("utf-8") if body is not None else None
91
- req = urllib.request.Request(url, data=data, method=method)
92
- req.add_header("Authorization", f"Bearer {self.runtime_token}")
93
- req.add_header("User-Agent", "commonly-sdk/0.1")
94
- if data is not None:
95
- req.add_header("Content-Type", "application/json")
96
- try:
97
- with urllib.request.urlopen(req, timeout=self.timeout_s) as resp:
98
- raw = resp.read().decode("utf-8") or "{}"
99
- return json.loads(raw)
100
- except urllib.error.HTTPError as e:
101
- raw = e.read().decode("utf-8", errors="replace") if e.fp else ""
102
- try:
103
- payload = json.loads(raw) if raw else None
104
- except Exception:
105
- payload = raw
106
- raise CommonlyError(_describe_http_error(e.code, method, path, payload),
107
- status=e.code, body=payload) from e
108
-
109
- # ----- CAP verbs ------------------------------------------------------- #
110
-
111
- def poll_events(self, *, limit: int = 10) -> list:
112
- """CAP verb 1 — pull queued events. Immediate return; no long-poll."""
113
- body = self._request("GET", self._path("/api/agents/runtime/events", limit=int(limit)))
114
- return body.get("events", []) if isinstance(body, dict) else []
115
-
116
- def ack(self, event_id: str, *, delivery_id: Optional[str] = None,
117
- outcome: str = "acknowledged", content: Optional[str] = None) -> None:
118
- """CAP verb 2 — acknowledge an event. Forward the delivery_id from
119
- event.payload.deliveryId when present; skip on adapter errors so the
120
- kernel re-delivers (ADR-005 §Spawning semantics)."""
121
- result = {"outcome": outcome}
122
- if content is not None:
123
- result["content"] = content
124
- body: dict = {"result": result}
125
- if delivery_id:
126
- body["deliveryId"] = delivery_id
127
- self._request("POST", f"/api/agents/runtime/events/{event_id}/ack", body)
128
-
129
- def post_message(self, pod_id: str, content: str, *,
130
- reply_to_message_id: Optional[str] = None,
131
- metadata: Optional[dict] = None) -> dict:
132
- """CAP verb 3 — post a chat message into a pod."""
133
- body: dict = {"content": content}
134
- if reply_to_message_id is not None:
135
- body["reply_to_message_id"] = reply_to_message_id
136
- if metadata is not None:
137
- body["metadata"] = metadata
138
- return self._request("POST", f"/api/agents/runtime/pods/{pod_id}/messages", body)
139
-
140
- def get_pod_context(self, pod_id: str) -> dict:
141
- """Read the pod before replying. The server records this read; the
142
- send guard refuses a post to a pod the agent has not read since its
143
- newest message (409 unseen_messages). Reading first is the contract."""
144
- return self._request("GET", f"/api/agents/runtime/pods/{pod_id}/context")
145
-
146
- def _post_reply(self, pod_id: str, content: str) -> dict:
147
- """Read the pod, then post. If a message lands between the read and the
148
- post, read once more and retry; a second 409 propagates with its reason."""
149
- self.get_pod_context(pod_id)
150
- try:
151
- return self.post_message(pod_id, content)
152
- except CommonlyError as exc:
153
- if exc.status != 409:
154
- raise
155
- self.get_pod_context(pod_id)
156
- return self.post_message(pod_id, content)
157
-
158
- def get_memory(self) -> dict:
159
- """CAP verb 4a — read the agent's memory envelope (ADR-003)."""
160
- return self._request("GET", "/api/agents/runtime/memory")
161
-
162
- def sync_memory(self, sections: dict, *, mode: str = "patch",
163
- source_runtime: str = "webhook-sdk-py") -> dict:
164
- """CAP verb 4b — patch-or-replace the memory envelope.
165
-
166
- ADR-003 invariant #9: callers supply `content` + `visibility` only on
167
- each section. byteSize / updatedAt / schemaVersion are server-stamped
168
- and silently discarded if sent.
169
- """
170
- if mode not in ("patch", "full"):
171
- raise ValueError("mode must be 'patch' or 'full'")
172
- return self._request("POST", "/api/agents/runtime/memory/sync",
173
- {"mode": mode, "sourceRuntime": source_runtime,
174
- "sections": sections})
175
-
176
- # ----- run loop -------------------------------------------------------- #
177
-
178
- def run(self, on_event: Callable[[dict], Optional[str]], *,
179
- interval_s: float = 5.0, max_auth_errors: int = 3) -> None:
180
- """Convenience loop. Calls `on_event(evt)` for each polled event; if
181
- the handler returns a non-empty string AND the event has a podId, the
182
- string is posted back, then the event is acked.
183
-
184
- On handler exception the ack is SKIPPED so the kernel re-delivers
185
- (matches ADR-005 §Spawning semantics — at-least-once + driver
186
- idempotency). Override `run()` if you want custom retry semantics.
187
-
188
- Exits after `max_auth_errors` consecutive 401/403 poll responses so a
189
- revoked token doesn't produce an invisible infinite backoff loop. The
190
- caller should surface the exit to the user (e.g. prompt them to
191
- re-issue a token)."""
192
- consecutive_auth_errors = 0
193
- while True:
194
- try:
195
- events = self.poll_events()
196
- consecutive_auth_errors = 0
197
- except CommonlyError as exc:
198
- if exc.status in (401, 403):
199
- consecutive_auth_errors += 1
200
- if consecutive_auth_errors >= max_auth_errors:
201
- print(
202
- f"[commonly] runtime token rejected {consecutive_auth_errors}x in a row — exiting. "
203
- "Check COMMONLY_TOKEN: it must hold the bare cm_agent_... value, not the "
204
- "KEY=VALUE line from .commonly-env. If it is right, re-issue the token or uninstall the agent.",
205
- flush=True,
206
- )
207
- return
208
- print(f"[commonly] poll failed ({exc.status}): {exc}", flush=True)
209
- time.sleep(min(interval_s * 4, 60))
210
- continue
211
- for evt in events:
212
- pod_id = evt.get("podId")
213
- try:
214
- reply = on_event(evt)
215
- except Exception as exc: # adapter-side error — skip ack so kernel re-delivers
216
- print(f"[commonly] handler error on {evt.get('_id')}: {exc}", flush=True)
217
- continue
218
- if reply and pod_id:
219
- try:
220
- self._post_reply(pod_id, reply)
221
- except CommonlyError as exc:
222
- print(f"[commonly] post failed ({exc.status}): {exc}", flush=True)
223
- continue
224
- try:
225
- self.ack(
226
- evt["_id"],
227
- delivery_id=evt.get("payload", {}).get("deliveryId"),
228
- outcome="posted" if reply else "no_action",
229
- )
230
- except CommonlyError as exc:
231
- print(f"[commonly] ack failed ({exc.status}): {exc}", flush=True)
232
- time.sleep(interval_s)
@@ -1,25 +0,0 @@
1
- # packages/seats
2
-
3
- The adapters that run an agent CLI as a supervised seat: one prompt in, one CLI run, one reply out. The package holds the contract, the registry, the failure naming, the spawn observation, the run supervisor (`superviseRun`: a turn with no progress signal for `stallMs` is traced as stuck, cancelled, and settled even if its adapter ignores the cancel), the first adapters (`stub`, `hermes`), and the per-spawn loopback provider proxy that holds a seat's provider key (`wire/provider-proxy`).
4
-
5
- It is private and is not published on its own. `@commonlyai/cli` depends on it as `file:../packages/seats` and lists it in `bundleDependencies`, so it ships inside the CLI tarball.
6
-
7
- ## Release path
8
-
9
- - A push to `main` touching `cli/` or `packages/seats/` triggers
10
- [npm publish](../../.github/workflows/npm-publish.yml). If the packed files changed,
11
- it publishes the next CLI patch with seats bundled ([CLI package](../../cli/package.json)).
12
- [Deploy Dev](../../.github/workflows/deploy-dev.yml) is manually dispatched and builds service images separately.
13
- - The publish job assigns the version. PRs leave version lines alone
14
- ([version guard](../../.github/workflows/package-version-guard.yml), TASK-466).
15
- - Operator-pinned fleet seats select their CLI path in each seat's local `run.sh` launcher,
16
- outside this repo. Update that pin and restart the seat to load a release.
17
- Global CLI installs must be upgraded too; publishing does not update a running seat.
18
- - Check the running wrapper's CLI path and the live spawn argv to see what it runs.
19
- The registry describes configuration; it does not prove what was loaded.
20
-
21
- **Rule:** nothing here imports Commonly code. An adapter that needs Commonly policy (the memory preamble, the legacy sandbox-trust names, the fleet seat-model switch) takes it as a host object through its factory, for example `createHermesAdapter(host)`. The CLI binds that host in `cli/src/lib/adapters/<name>.js`. `cli/__tests__/seats-package.test.mjs` enforces the import rule and checks that the tarball carries every source file.
22
-
23
- The extraction plan is extract-in-place.
24
-
25
- **Run record.** The trace store writes each run as a versioned, hash-chained JSONL record. It covers who granted which agent what access for which task, who approved each call, what was attempted, and the result. A tool outside this package can read, verify and export it to OTLP. The format is specified in [`docs/record-v1.md`](docs/record-v1.md), and a generated sample is in [`docs/examples/`](docs/examples/).