@stitchdb/cli 0.1.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/README.md +43 -0
- package/dist/cli.js +492 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @stitchdb/cli
|
|
2
|
+
|
|
3
|
+
`stitch` — memory + agent control plane CLI.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @stitchdb/cli
|
|
9
|
+
stitch login # paste your API key
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Memory
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
stitch remember "We chose Drizzle over Prisma" --kind decision --tag db
|
|
16
|
+
stitch recall "which ORM do we use?"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Run an agent on this machine
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# 1. Register an agent identity (one-time, returns an id):
|
|
23
|
+
stitch agent register my-laptop
|
|
24
|
+
|
|
25
|
+
# 2. Hold open the control channel — keep this running:
|
|
26
|
+
stitch agent run --id agt_a1b2c3d4e5f60789
|
|
27
|
+
|
|
28
|
+
# 3. From any other machine, dispatch a command:
|
|
29
|
+
stitch agent dispatch agt_a1b2c3d4e5f60789 "summarize the last commit"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
By default `stitch agent run` will execute `claude -p "<prompt>"` for `prompt`-kind dispatches. Shell execution is opt-in via `--allow-shell`.
|
|
33
|
+
|
|
34
|
+
## Add to Claude Code
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
stitch mcp-install
|
|
38
|
+
# prints the `claude mcp add stitch …` command
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `stitch` — CLI for the Stitch memory + agent control plane.
|
|
4
|
+
*
|
|
5
|
+
* stitch login Save your API key locally.
|
|
6
|
+
* stitch logout Remove the saved key.
|
|
7
|
+
* stitch whoami Show the configured key (masked).
|
|
8
|
+
*
|
|
9
|
+
* stitch remember <text> [--kind X] [--tag a,b]
|
|
10
|
+
* stitch recall <query> [-k 5]
|
|
11
|
+
*
|
|
12
|
+
* stitch agent register <name> Create a new agent identity.
|
|
13
|
+
* stitch agent list List your agents (online status).
|
|
14
|
+
* stitch agent run --id <agent_id> Hold open a control channel + execute dispatched commands.
|
|
15
|
+
* stitch agent dispatch <id> <prompt> Send a command to a connected agent.
|
|
16
|
+
* stitch agent revoke <id> Revoke an agent.
|
|
17
|
+
*
|
|
18
|
+
* stitch workspace create <name>
|
|
19
|
+
* stitch workspace list
|
|
20
|
+
* stitch workspace use <id> Set the default workspace.
|
|
21
|
+
*
|
|
22
|
+
* stitch mcp-install [--id <agent_id>] Print the `claude mcp add stitch …` command for your workspace.
|
|
23
|
+
*/
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import * as os from 'node:os';
|
|
27
|
+
import { spawn } from 'node:child_process';
|
|
28
|
+
import { Stitch } from '@stitchdb/agent';
|
|
29
|
+
const CONFIG_DIR = path.join(os.homedir(), '.stitch');
|
|
30
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
31
|
+
function loadConfig() {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function saveConfig(c) {
|
|
40
|
+
if (!fs.existsSync(CONFIG_DIR))
|
|
41
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
42
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(c, null, 2), { mode: 0o600 });
|
|
43
|
+
}
|
|
44
|
+
function mask(key) {
|
|
45
|
+
if (!key)
|
|
46
|
+
return '(none)';
|
|
47
|
+
if (key.length < 12)
|
|
48
|
+
return '****';
|
|
49
|
+
return key.slice(0, 7) + '…' + key.slice(-4);
|
|
50
|
+
}
|
|
51
|
+
function client(cfg) {
|
|
52
|
+
if (!cfg.apiKey) {
|
|
53
|
+
console.error('Not logged in. Run: stitch login');
|
|
54
|
+
process.exit(2);
|
|
55
|
+
}
|
|
56
|
+
return new Stitch({
|
|
57
|
+
apiKey: cfg.apiKey,
|
|
58
|
+
baseUrl: cfg.baseUrl,
|
|
59
|
+
workspace: cfg.defaultWorkspace,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// ─── Argv helpers ──────────────────────────────────────────────────────────
|
|
63
|
+
function parseFlag(args, names) {
|
|
64
|
+
for (const name of names) {
|
|
65
|
+
const i = args.findIndex((a) => a === name || a.startsWith(name + '='));
|
|
66
|
+
if (i === -1)
|
|
67
|
+
continue;
|
|
68
|
+
if (args[i].includes('='))
|
|
69
|
+
return args[i].split('=', 2)[1];
|
|
70
|
+
return args[i + 1];
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
function hasFlag(args, names) {
|
|
75
|
+
return args.some((a) => names.some((n) => a === n));
|
|
76
|
+
}
|
|
77
|
+
function positional(args) {
|
|
78
|
+
return args.filter((a) => !a.startsWith('-'));
|
|
79
|
+
}
|
|
80
|
+
async function readJsonBody(res) {
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(text);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
throw new Error(text);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ─── Commands ──────────────────────────────────────────────────────────────
|
|
90
|
+
async function cmdLogin(args) {
|
|
91
|
+
const fromFlag = parseFlag(args, ['--key', '-k']);
|
|
92
|
+
const fromEnv = process.env.STITCHDB_API_KEY;
|
|
93
|
+
let key = fromFlag || fromEnv;
|
|
94
|
+
if (!key) {
|
|
95
|
+
process.stdout.write('Paste your StitchDB API key: ');
|
|
96
|
+
key = await readLine();
|
|
97
|
+
}
|
|
98
|
+
key = (key || '').trim();
|
|
99
|
+
if (!key.startsWith('sdb_sk_')) {
|
|
100
|
+
console.error('That does not look like a Stitch key (should start with sdb_sk_).');
|
|
101
|
+
process.exit(2);
|
|
102
|
+
}
|
|
103
|
+
const cfg = loadConfig();
|
|
104
|
+
cfg.apiKey = key;
|
|
105
|
+
cfg.baseUrl = cfg.baseUrl || 'https://db.stitchdb.com';
|
|
106
|
+
saveConfig(cfg);
|
|
107
|
+
console.log(`Saved key ${mask(key)} to ${CONFIG_FILE}`);
|
|
108
|
+
}
|
|
109
|
+
async function cmdLogout() {
|
|
110
|
+
const cfg = loadConfig();
|
|
111
|
+
delete cfg.apiKey;
|
|
112
|
+
delete cfg.defaultWorkspace;
|
|
113
|
+
saveConfig(cfg);
|
|
114
|
+
console.log('Logged out.');
|
|
115
|
+
}
|
|
116
|
+
async function cmdWhoami() {
|
|
117
|
+
const cfg = loadConfig();
|
|
118
|
+
console.log('API key: ', mask(cfg.apiKey || ''));
|
|
119
|
+
console.log('Base URL: ', cfg.baseUrl || 'https://db.stitchdb.com');
|
|
120
|
+
console.log('Workspace: ', cfg.defaultWorkspace || '(none — first will auto-create)');
|
|
121
|
+
console.log('Config file:', CONFIG_FILE);
|
|
122
|
+
}
|
|
123
|
+
async function cmdRemember(args) {
|
|
124
|
+
const cfg = loadConfig();
|
|
125
|
+
const stitch = client(cfg);
|
|
126
|
+
const positionals = positional(args);
|
|
127
|
+
const content = positionals.join(' ');
|
|
128
|
+
if (!content) {
|
|
129
|
+
console.error('Usage: stitch remember <text>');
|
|
130
|
+
process.exit(2);
|
|
131
|
+
}
|
|
132
|
+
const kind = parseFlag(args, ['--kind']);
|
|
133
|
+
const tagFlag = parseFlag(args, ['--tag', '--tags']);
|
|
134
|
+
const tags = tagFlag ? tagFlag.split(',').map((s) => s.trim()).filter(Boolean) : undefined;
|
|
135
|
+
const out = await stitch.remember(content, { kind, tags });
|
|
136
|
+
console.log(`Stored ${out.id}${out.embedded ? '' : ' (no embedding — degraded mode)'}`);
|
|
137
|
+
}
|
|
138
|
+
async function cmdRecall(args) {
|
|
139
|
+
const cfg = loadConfig();
|
|
140
|
+
const stitch = client(cfg);
|
|
141
|
+
const positionals = positional(args);
|
|
142
|
+
const query = positionals.join(' ');
|
|
143
|
+
if (!query) {
|
|
144
|
+
console.error('Usage: stitch recall <query> [-k 5]');
|
|
145
|
+
process.exit(2);
|
|
146
|
+
}
|
|
147
|
+
const k = Number(parseFlag(args, ['-k', '--k']) || '5');
|
|
148
|
+
const hits = await stitch.recall(query, { k });
|
|
149
|
+
if (hits.length === 0) {
|
|
150
|
+
console.log('No matches.');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
for (const [i, h] of hits.entries()) {
|
|
154
|
+
console.log(`${(i + 1).toString().padStart(2)}. [${h.kind}] (score ${h.score.toFixed(3)}, ${h.id})`);
|
|
155
|
+
console.log(' ' + h.content.split('\n').join('\n '));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function cmdWorkspace(args) {
|
|
159
|
+
const cfg = loadConfig();
|
|
160
|
+
const stitch = client(cfg);
|
|
161
|
+
const sub = args[0];
|
|
162
|
+
const rest = args.slice(1);
|
|
163
|
+
if (sub === 'list') {
|
|
164
|
+
const list = await stitch.workspaces.list();
|
|
165
|
+
for (const w of list)
|
|
166
|
+
console.log(`${w.id} ${w.name}`);
|
|
167
|
+
}
|
|
168
|
+
else if (sub === 'create') {
|
|
169
|
+
const name = positional(rest).join(' ') || 'default';
|
|
170
|
+
const w = await stitch.workspaces.create(name);
|
|
171
|
+
console.log(`Created ${w.id} ${w.name}`);
|
|
172
|
+
}
|
|
173
|
+
else if (sub === 'use') {
|
|
174
|
+
const id = rest[0];
|
|
175
|
+
if (!id) {
|
|
176
|
+
console.error('Usage: stitch workspace use <id>');
|
|
177
|
+
process.exit(2);
|
|
178
|
+
}
|
|
179
|
+
cfg.defaultWorkspace = id;
|
|
180
|
+
saveConfig(cfg);
|
|
181
|
+
console.log(`Default workspace set: ${id}`);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
console.error('Usage: stitch workspace [list|create <name>|use <id>]');
|
|
185
|
+
process.exit(2);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function cmdAgent(args) {
|
|
189
|
+
const cfg = loadConfig();
|
|
190
|
+
if (!cfg.apiKey) {
|
|
191
|
+
console.error('Not logged in. Run: stitch login');
|
|
192
|
+
process.exit(2);
|
|
193
|
+
}
|
|
194
|
+
const sub = args[0];
|
|
195
|
+
const rest = args.slice(1);
|
|
196
|
+
const baseUrl = cfg.baseUrl || 'https://db.stitchdb.com';
|
|
197
|
+
if (sub === 'register') {
|
|
198
|
+
const name = positional(rest).join(' ') || os.hostname();
|
|
199
|
+
const res = await fetch(`${baseUrl}/v1/agent/agents`, {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json' },
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
name,
|
|
204
|
+
machine: `${os.platform()} ${os.arch()} (${os.hostname()})`,
|
|
205
|
+
version: 'cli-0.1.0',
|
|
206
|
+
capabilities: ['claude_code', 'shell'],
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
const body = await readJsonBody(res);
|
|
210
|
+
if (!res.ok) {
|
|
211
|
+
console.error(body);
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
214
|
+
console.log(`Agent registered: ${body.agent.id}`);
|
|
215
|
+
console.log(`Run on the target machine:`);
|
|
216
|
+
console.log(` stitch agent run --id ${body.agent.id}`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (sub === 'list') {
|
|
220
|
+
const res = await fetch(`${baseUrl}/v1/agent/agents`, {
|
|
221
|
+
headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
|
|
222
|
+
});
|
|
223
|
+
const body = await readJsonBody(res);
|
|
224
|
+
if (!res.ok) {
|
|
225
|
+
console.error(body);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
if (body.agents.length === 0) {
|
|
229
|
+
console.log('No agents yet. Try: stitch agent register <name>');
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
for (const a of body.agents) {
|
|
233
|
+
const status = a.online ? '● online' : (a.revoked_at ? 'revoked' : '○ offline');
|
|
234
|
+
const ago = a.last_seen_at ? humanAgo(Date.now() - a.last_seen_at) : 'never';
|
|
235
|
+
console.log(`${a.id} ${status.padEnd(10)} ${a.name.padEnd(24)} last seen ${ago}`);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (sub === 'run') {
|
|
240
|
+
const agentId = parseFlag(rest, ['--id', '-i']);
|
|
241
|
+
if (!agentId) {
|
|
242
|
+
console.error('Usage: stitch agent run --id <agent_id>');
|
|
243
|
+
process.exit(2);
|
|
244
|
+
}
|
|
245
|
+
const allowShell = hasFlag(rest, ['--allow-shell']);
|
|
246
|
+
return runAgent(cfg, agentId, { allowShell });
|
|
247
|
+
}
|
|
248
|
+
if (sub === 'dispatch') {
|
|
249
|
+
const positionals = positional(rest);
|
|
250
|
+
const agentId = positionals[0];
|
|
251
|
+
const prompt = positionals.slice(1).join(' ');
|
|
252
|
+
if (!agentId || !prompt) {
|
|
253
|
+
console.error('Usage: stitch agent dispatch <agent_id> <prompt>');
|
|
254
|
+
process.exit(2);
|
|
255
|
+
}
|
|
256
|
+
const kind = parseFlag(rest, ['--kind']) || 'prompt';
|
|
257
|
+
const cwd = parseFlag(rest, ['--cwd']);
|
|
258
|
+
const timeoutMs = Number(parseFlag(rest, ['--timeout']) || '120000');
|
|
259
|
+
const payload = { kind, timeoutMs };
|
|
260
|
+
if (kind === 'shell')
|
|
261
|
+
payload.command = prompt;
|
|
262
|
+
else
|
|
263
|
+
payload.prompt = prompt;
|
|
264
|
+
if (cwd)
|
|
265
|
+
payload.cwd = cwd;
|
|
266
|
+
const res = await fetch(`${baseUrl}/v1/agent/agents/${encodeURIComponent(agentId)}/dispatch`, {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
headers: { 'Authorization': `Bearer ${cfg.apiKey}`, 'Content-Type': 'application/json' },
|
|
269
|
+
body: JSON.stringify(payload),
|
|
270
|
+
});
|
|
271
|
+
const body = await readJsonBody(res);
|
|
272
|
+
if (!res.ok) {
|
|
273
|
+
console.error(body);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
if (body.error) {
|
|
277
|
+
console.error('Error:', body.error);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
if (body.stdout)
|
|
281
|
+
process.stdout.write(body.stdout);
|
|
282
|
+
if (body.stderr)
|
|
283
|
+
process.stderr.write(body.stderr);
|
|
284
|
+
if (typeof body.exit_code === 'number')
|
|
285
|
+
process.exit(body.exit_code);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (sub === 'revoke') {
|
|
289
|
+
const id = rest[0];
|
|
290
|
+
if (!id) {
|
|
291
|
+
console.error('Usage: stitch agent revoke <id>');
|
|
292
|
+
process.exit(2);
|
|
293
|
+
}
|
|
294
|
+
const res = await fetch(`${baseUrl}/v1/agent/agents/${encodeURIComponent(id)}`, {
|
|
295
|
+
method: 'DELETE',
|
|
296
|
+
headers: { 'Authorization': `Bearer ${cfg.apiKey}` },
|
|
297
|
+
});
|
|
298
|
+
const body = await readJsonBody(res);
|
|
299
|
+
if (!res.ok) {
|
|
300
|
+
console.error(body);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
console.log(`Revoked ${id}`);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
console.error('Usage: stitch agent [register|list|run|dispatch|revoke] …');
|
|
307
|
+
process.exit(2);
|
|
308
|
+
}
|
|
309
|
+
async function cmdMcpInstall(args) {
|
|
310
|
+
const cfg = loadConfig();
|
|
311
|
+
if (!cfg.apiKey) {
|
|
312
|
+
console.error('Not logged in. Run: stitch login');
|
|
313
|
+
process.exit(2);
|
|
314
|
+
}
|
|
315
|
+
const stitch = client(cfg);
|
|
316
|
+
const ws = await stitch.resolveWorkspace();
|
|
317
|
+
const baseUrl = cfg.baseUrl || 'https://db.stitchdb.com';
|
|
318
|
+
console.log('# Add Stitch as an MCP server in Claude Code:');
|
|
319
|
+
console.log(`claude mcp add stitch \\`);
|
|
320
|
+
console.log(` ${baseUrl}/mcp/v1/${ws} \\`);
|
|
321
|
+
console.log(` --transport http \\`);
|
|
322
|
+
console.log(` --header "Authorization: Bearer ${cfg.apiKey}"`);
|
|
323
|
+
console.log();
|
|
324
|
+
console.log('# Or for Cursor / Continue / Aider — add the same URL with the same Authorization header.');
|
|
325
|
+
}
|
|
326
|
+
// ─── Agent run loop ─────────────────────────────────────────────────────────
|
|
327
|
+
async function runAgent(cfg, agentId, opts) {
|
|
328
|
+
const baseUrl = (cfg.baseUrl || 'https://db.stitchdb.com').replace(/^http/, 'ws');
|
|
329
|
+
const url = `${baseUrl}/ws/agent?id=${encodeURIComponent(agentId)}&key=${encodeURIComponent(cfg.apiKey)}`;
|
|
330
|
+
let connection = 0;
|
|
331
|
+
let backoff = 1000;
|
|
332
|
+
const connect = () => new Promise((resolveOuter) => {
|
|
333
|
+
connection++;
|
|
334
|
+
const startedAt = Date.now();
|
|
335
|
+
const ws = new WebSocket(url);
|
|
336
|
+
ws.addEventListener('open', () => {
|
|
337
|
+
backoff = 1000;
|
|
338
|
+
console.log(`[stitch agent] online — id=${agentId} (connection #${connection})`);
|
|
339
|
+
// Send initial status report.
|
|
340
|
+
ws.send(JSON.stringify({
|
|
341
|
+
type: 'status',
|
|
342
|
+
status: {
|
|
343
|
+
cwd: process.cwd(),
|
|
344
|
+
platform: process.platform,
|
|
345
|
+
arch: process.arch,
|
|
346
|
+
host: os.hostname(),
|
|
347
|
+
allow_shell: opts.allowShell,
|
|
348
|
+
pid: process.pid,
|
|
349
|
+
},
|
|
350
|
+
}));
|
|
351
|
+
// Heartbeat every 30s.
|
|
352
|
+
const hb = setInterval(() => {
|
|
353
|
+
if (ws.readyState === ws.OPEN)
|
|
354
|
+
ws.send(JSON.stringify({ type: 'heartbeat' }));
|
|
355
|
+
}, 30_000);
|
|
356
|
+
ws.__hb = hb;
|
|
357
|
+
});
|
|
358
|
+
ws.addEventListener('message', async (e) => {
|
|
359
|
+
let msg;
|
|
360
|
+
try {
|
|
361
|
+
msg = JSON.parse(typeof e.data === 'string' ? e.data : new TextDecoder().decode(e.data));
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (msg.type === 'dispatch') {
|
|
367
|
+
const { taskId } = msg;
|
|
368
|
+
try {
|
|
369
|
+
const result = await executeDispatch(msg, opts);
|
|
370
|
+
ws.send(JSON.stringify({ type: 'response', taskId, result, final: true }));
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
ws.send(JSON.stringify({ type: 'response', taskId, error: err?.message || String(err), final: true }));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
ws.addEventListener('error', () => { });
|
|
378
|
+
ws.addEventListener('close', () => {
|
|
379
|
+
clearInterval(ws.__hb);
|
|
380
|
+
const lifespan = Date.now() - startedAt;
|
|
381
|
+
console.error(`[stitch agent] disconnected (lived ${Math.round(lifespan / 1000)}s) — reconnecting in ${backoff}ms`);
|
|
382
|
+
setTimeout(() => connect().then(resolveOuter), backoff);
|
|
383
|
+
backoff = Math.min(backoff * 2, 30_000);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
// Hold the process open forever (until SIGINT).
|
|
387
|
+
await connect();
|
|
388
|
+
await new Promise(() => { });
|
|
389
|
+
}
|
|
390
|
+
async function executeDispatch(input, opts) {
|
|
391
|
+
if (input.kind === 'prompt') {
|
|
392
|
+
if (!input.prompt)
|
|
393
|
+
throw new Error('prompt is required');
|
|
394
|
+
// Spawn `claude -p <prompt>` if Claude Code is on PATH.
|
|
395
|
+
const claudeBin = process.env.STITCH_CLAUDE_BIN || 'claude';
|
|
396
|
+
const out = await run(claudeBin, ['-p', input.prompt], { cwd: input.cwd });
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
if (input.kind === 'shell') {
|
|
400
|
+
if (!opts.allowShell)
|
|
401
|
+
throw new Error('Shell execution not enabled. Run with --allow-shell to opt in.');
|
|
402
|
+
if (!input.command)
|
|
403
|
+
throw new Error('command is required');
|
|
404
|
+
const out = await run('/bin/sh', ['-c', input.command], { cwd: input.cwd, env: input.env });
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
if (input.kind === 'ping') {
|
|
408
|
+
return { ok: true, ts: Date.now() };
|
|
409
|
+
}
|
|
410
|
+
throw new Error(`Unknown dispatch kind: ${input.kind}`);
|
|
411
|
+
}
|
|
412
|
+
function run(cmd, args, opts = {}) {
|
|
413
|
+
return new Promise((resolve) => {
|
|
414
|
+
const child = spawn(cmd, args, {
|
|
415
|
+
cwd: opts.cwd || process.cwd(),
|
|
416
|
+
env: { ...process.env, ...(opts.env || {}) },
|
|
417
|
+
});
|
|
418
|
+
let stdout = '';
|
|
419
|
+
let stderr = '';
|
|
420
|
+
child.stdout.on('data', (d) => stdout += d.toString());
|
|
421
|
+
child.stderr.on('data', (d) => stderr += d.toString());
|
|
422
|
+
child.on('error', (err) => resolve({ stdout, stderr: stderr + '\n[spawn error] ' + err.message, exit_code: 127 }));
|
|
423
|
+
child.on('close', (code) => resolve({ stdout, stderr, exit_code: code ?? 0 }));
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
function readLine() {
|
|
427
|
+
return new Promise((resolve) => {
|
|
428
|
+
const onData = (d) => { process.stdin.off('data', onData); resolve(d.toString().trim()); };
|
|
429
|
+
process.stdin.on('data', onData);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function humanAgo(ms) {
|
|
433
|
+
if (ms < 60_000)
|
|
434
|
+
return Math.round(ms / 1000) + 's ago';
|
|
435
|
+
if (ms < 3_600_000)
|
|
436
|
+
return Math.round(ms / 60_000) + 'm ago';
|
|
437
|
+
if (ms < 86_400_000)
|
|
438
|
+
return Math.round(ms / 3_600_000) + 'h ago';
|
|
439
|
+
return Math.round(ms / 86_400_000) + 'd ago';
|
|
440
|
+
}
|
|
441
|
+
function help() {
|
|
442
|
+
console.log(`stitch — memory + agent control plane
|
|
443
|
+
|
|
444
|
+
stitch login Save your API key.
|
|
445
|
+
stitch whoami Show the configured key.
|
|
446
|
+
stitch logout
|
|
447
|
+
|
|
448
|
+
stitch remember <text> [--kind X] [--tag a,b]
|
|
449
|
+
stitch recall <query> [-k 5]
|
|
450
|
+
|
|
451
|
+
stitch workspace [list | create <name> | use <id>]
|
|
452
|
+
|
|
453
|
+
stitch agent register <name> Create an agent identity (id only).
|
|
454
|
+
stitch agent list
|
|
455
|
+
stitch agent run --id <agent_id> [--allow-shell]
|
|
456
|
+
stitch agent dispatch <agent_id> <prompt> [--kind prompt|shell] [--cwd path]
|
|
457
|
+
stitch agent revoke <agent_id>
|
|
458
|
+
|
|
459
|
+
stitch mcp-install Print the Claude Code MCP install command.
|
|
460
|
+
`);
|
|
461
|
+
}
|
|
462
|
+
// ─── Entry ─────────────────────────────────────────────────────────────────
|
|
463
|
+
async function main(argv) {
|
|
464
|
+
const [cmd, ...rest] = argv;
|
|
465
|
+
try {
|
|
466
|
+
switch (cmd) {
|
|
467
|
+
case 'login': return cmdLogin(rest);
|
|
468
|
+
case 'logout': return cmdLogout();
|
|
469
|
+
case 'whoami': return cmdWhoami();
|
|
470
|
+
case 'remember': return cmdRemember(rest);
|
|
471
|
+
case 'recall': return cmdRecall(rest);
|
|
472
|
+
case 'workspace':
|
|
473
|
+
case 'ws': return cmdWorkspace(rest);
|
|
474
|
+
case 'agent': return cmdAgent(rest);
|
|
475
|
+
case 'mcp-install':
|
|
476
|
+
case 'mcp': return cmdMcpInstall(rest);
|
|
477
|
+
case 'help':
|
|
478
|
+
case '--help':
|
|
479
|
+
case '-h':
|
|
480
|
+
case undefined: return help();
|
|
481
|
+
default:
|
|
482
|
+
console.error(`Unknown command: ${cmd}`);
|
|
483
|
+
help();
|
|
484
|
+
process.exit(2);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
catch (e) {
|
|
488
|
+
console.error('Error:', e?.message || e);
|
|
489
|
+
process.exit(1);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
main(process.argv.slice(2));
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stitchdb/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Stitch CLI — manage memory + run agents from your terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"stitch": "dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["dist", "README.md"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"prepublishOnly": "npm run build",
|
|
13
|
+
"dev": "tsx src/cli.ts"
|
|
14
|
+
},
|
|
15
|
+
"keywords": ["mcp", "ai", "agent", "claude-code", "memory", "stitchdb"],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"engines": { "node": ">=20" },
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@stitchdb/agent": "^0.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"typescript": "^5.4.0",
|
|
23
|
+
"@types/node": "^22.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|