@zlnap/astrolabe 0.0.0-stage → 0.17.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.
@@ -0,0 +1,539 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const test = require('node:test');
8
+ const { spawnSync } = require('node:child_process');
9
+
10
+ const argsMod = require('./setup/args');
11
+ const clientsMod = require('./setup/clients');
12
+ const { CLIENTS, CATALOG_TOOL_NAMES } = clientsMod;
13
+
14
+ const BIN = path.join(__dirname, '..', 'bin', 'astrolabe.js');
15
+
16
+ function tmpHome() {
17
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'astrolabe-setup-'));
18
+ }
19
+
20
+ test('parseArgs accepts yes/clients/dry-run/native', () => {
21
+ const opts = argsMod.parseArgs([
22
+ '--yes',
23
+ '--clients=cursor,codex',
24
+ '--dry-run',
25
+ '--native',
26
+ '--home',
27
+ '/tmp/h',
28
+ ]);
29
+ assert.equal(opts.yes, true);
30
+ assert.deepEqual(opts.clients, ['cursor', 'codex']);
31
+ assert.equal(opts.dryRun, true);
32
+ assert.equal(opts.native, true);
33
+ assert.equal(opts.home, '/tmp/h');
34
+ });
35
+
36
+ test('resolveLaunch defaults to npx @zlnap/astrolabe', () => {
37
+ assert.deepEqual(clientsMod.resolveLaunch({ context: 'cursor' }), {
38
+ command: 'npx',
39
+ args: ['-y', '@zlnap/astrolabe', '.', '--context=cursor'],
40
+ });
41
+ assert.deepEqual(clientsMod.resolveLaunch({ context: null, native: true }), {
42
+ command: 'astrolabe',
43
+ args: ['.'],
44
+ });
45
+ });
46
+
47
+ test('mergeJsonMcpFile preserves other servers', () => {
48
+ const home = tmpHome();
49
+ try {
50
+ const filePath = path.join(home, 'mcp.json');
51
+ fs.writeFileSync(
52
+ filePath,
53
+ JSON.stringify({ mcpServers: { other: { command: 'x', args: [] } } }, null, 2),
54
+ );
55
+ const entry = { command: 'npx', args: ['-y', '@zlnap/astrolabe', '.', '--context=cursor'] };
56
+ const result = clientsMod.mergeJsonMcpFile(filePath, entry);
57
+ assert.equal(result.created, false);
58
+ assert.equal(result.doc.mcpServers.other.command, 'x');
59
+ assert.deepEqual(result.doc.mcpServers.astrolabe, entry);
60
+ } finally {
61
+ fs.rmSync(home, { recursive: true, force: true });
62
+ }
63
+ });
64
+
65
+ test('mergeCodexToml appends and replaces astrolabe table only', () => {
66
+ const home = tmpHome();
67
+ try {
68
+ const filePath = path.join(home, 'config.toml');
69
+ fs.writeFileSync(
70
+ filePath,
71
+ 'model = "x"\n\n[mcp_servers.other]\ncommand = "o"\nargs = ["1"]\n',
72
+ );
73
+ const launch = { command: 'npx', args: ['-y', '@zlnap/astrolabe', '.', '--context=codex'] };
74
+ const first = clientsMod.mergeCodexToml(filePath, launch);
75
+ assert.match(first.text, /\[mcp_servers\.astrolabe\]/);
76
+ assert.match(first.text, /\[mcp_servers\.other\]/);
77
+ assert.match(first.text, /model = "x"/);
78
+
79
+ fs.writeFileSync(filePath, first.text);
80
+ const second = clientsMod.mergeCodexToml(filePath, {
81
+ command: 'astrolabe',
82
+ args: ['.', '--context=codex'],
83
+ });
84
+ assert.equal(second.replaced, true);
85
+ assert.match(second.text, /command = "astrolabe"/);
86
+ assert.equal((second.text.match(/\[mcp_servers\.astrolabe\]/g) || []).length, 1);
87
+ assert.match(second.text, /\[mcp_servers\.other\]/);
88
+ } finally {
89
+ fs.rmSync(home, { recursive: true, force: true });
90
+ }
91
+ });
92
+
93
+ test('CLIENTS catalog covers documented agents', () => {
94
+ const ids = CLIENTS.map((c) => c.id).sort();
95
+ assert.deepEqual(ids, [
96
+ 'claude-code',
97
+ 'cline',
98
+ 'codebuddy',
99
+ 'codex',
100
+ 'continue',
101
+ 'cursor',
102
+ 'deepseek-harness',
103
+ 'grok',
104
+ 'kimicode',
105
+ 'opencode',
106
+ 'roo',
107
+ 'windsurf',
108
+ 'zcode',
109
+ ]);
110
+ assert.equal(CATALOG_TOOL_NAMES.length, 7);
111
+ assert.equal(clientsMod.canonicalClientId('kimi'), 'kimicode');
112
+ assert.equal(clientsMod.canonicalClientId('zai'), 'zcode');
113
+ assert.equal(clientsMod.canonicalClientId('grokbuild'), 'grok');
114
+ });
115
+
116
+ test('setup --list and --dry-run via bin shim', () => {
117
+ const home = tmpHome();
118
+ try {
119
+ fs.mkdirSync(path.join(home, '.cursor'), { recursive: true });
120
+ fs.writeFileSync(
121
+ path.join(home, '.cursor', 'mcp.json'),
122
+ JSON.stringify({ mcpServers: { keep: { command: 'keep' } } }, null, 2),
123
+ );
124
+ const list = spawnSync(process.execPath, [BIN, 'setup', '--list', '--home', home], {
125
+ encoding: 'utf8',
126
+ });
127
+ assert.equal(list.status, 0, list.stderr);
128
+ assert.match(list.stdout, /cursor\s+detected/);
129
+
130
+ const dry = spawnSync(
131
+ process.execPath,
132
+ [BIN, 'setup', '--yes', '--clients=cursor', '--dry-run', '--home', home, '--no-cursor-permissions'],
133
+ { encoding: 'utf8' },
134
+ );
135
+ assert.equal(dry.status, 0, dry.stderr + dry.stdout);
136
+ assert.match(dry.stdout, /\[dry-run\]/);
137
+ const doc = JSON.parse(fs.readFileSync(path.join(home, '.cursor', 'mcp.json'), 'utf8'));
138
+ assert.equal(doc.mcpServers.keep.command, 'keep');
139
+ assert.equal(doc.mcpServers.astrolabe, undefined);
140
+ } finally {
141
+ fs.rmSync(home, { recursive: true, force: true });
142
+ }
143
+ });
144
+
145
+ test('setup --yes writes cursor mcp.json and merges allowlist', () => {
146
+ const home = tmpHome();
147
+ try {
148
+ fs.mkdirSync(path.join(home, '.cursor'), { recursive: true });
149
+ fs.writeFileSync(
150
+ path.join(home, '.cursor', 'mcp.json'),
151
+ JSON.stringify({ mcpServers: { keep: { command: 'keep' } } }, null, 2),
152
+ );
153
+ const result = spawnSync(
154
+ process.execPath,
155
+ [BIN, 'setup', '--yes', '--clients=cursor', '--home', home],
156
+ { encoding: 'utf8' },
157
+ );
158
+ assert.equal(result.status, 0, result.stderr + result.stdout);
159
+ const mcp = JSON.parse(fs.readFileSync(path.join(home, '.cursor', 'mcp.json'), 'utf8'));
160
+ assert.equal(mcp.mcpServers.keep.command, 'keep');
161
+ assert.equal(mcp.mcpServers.astrolabe.command, 'npx');
162
+ assert.deepEqual(mcp.mcpServers.astrolabe.args, [
163
+ '-y',
164
+ '@zlnap/astrolabe',
165
+ '.',
166
+ '--context=cursor',
167
+ ]);
168
+ const perms = JSON.parse(
169
+ fs.readFileSync(path.join(home, '.cursor', 'permissions.json'), 'utf8'),
170
+ );
171
+ assert.ok(perms.mcpAllowlist.includes('astrolabe:search_code'));
172
+ assert.equal(perms.mcpAllowlist.length, 7);
173
+ } finally {
174
+ fs.rmSync(home, { recursive: true, force: true });
175
+ }
176
+ });
177
+
178
+ test('detect uses PATH and home markers', () => {
179
+ const home = tmpHome();
180
+ try {
181
+ fs.mkdirSync(path.join(home, '.codex'), { recursive: true });
182
+ const env = { ...process.env, PATH: '/nonexistent/bin', HOME: home };
183
+ const codex = CLIENTS.find((c) => c.id === 'codex').detect(home, env);
184
+ assert.equal(codex.detected, true);
185
+ const windsurf = CLIENTS.find((c) => c.id === 'windsurf').detect(home, env);
186
+ assert.equal(windsurf.detected, false);
187
+ } finally {
188
+ fs.rmSync(home, { recursive: true, force: true });
189
+ }
190
+ });
191
+
192
+ test('parseArgs accepts --with-hooks', () => {
193
+ const opts = argsMod.parseArgs(['--with-hooks', '--clients=dsh,opencode']);
194
+ assert.equal(opts.withHooks, true);
195
+ assert.deepEqual(opts.clients, ['dsh', 'opencode']);
196
+ assert.equal(clientsMod.getClient('dsh').id, 'deepseek-harness');
197
+ assert.equal(clientsMod.canonicalClientId('dsh'), 'deepseek-harness');
198
+ });
199
+
200
+ test('OpenCode V1 merge preserves JSONC comments and siblings', () => {
201
+ const home = tmpHome();
202
+ try {
203
+ const dir = path.join(home, '.config', 'opencode');
204
+ fs.mkdirSync(dir, { recursive: true });
205
+ fs.writeFileSync(
206
+ path.join(dir, 'opencode.jsonc'),
207
+ `{
208
+ // keep-comment
209
+ "mcp": {
210
+ "serena": { "type": "local", "command": ["echo"], "enabled": true }
211
+ }
212
+ }
213
+ `,
214
+ );
215
+ const r = clientsMod.mergeOpenCodeMcpFile(path.join(dir, 'opencode.jsonc'), {
216
+ command: 'npx',
217
+ args: ['-y', '@zlnap/astrolabe', '.'],
218
+ });
219
+ assert.equal(r.format, 'v1');
220
+ assert.match(r.text, /keep-comment/);
221
+ assert.match(r.text, /"serena"/);
222
+ assert.match(r.text, /"astrolabe"/);
223
+ assert.match(r.text, /"enabled"\s*:\s*true/);
224
+ } finally {
225
+ fs.rmSync(home, { recursive: true, force: true });
226
+ }
227
+ });
228
+
229
+ test('OpenCode V2 merge writes mcp.servers without enabled', () => {
230
+ const home = tmpHome();
231
+ try {
232
+ const file = path.join(home, 'opencode.jsonc');
233
+ fs.writeFileSync(
234
+ file,
235
+ `{
236
+ "mcp": {
237
+ "servers": {
238
+ "other": { "type": "local", "command": ["x"] }
239
+ }
240
+ }
241
+ }
242
+ `,
243
+ );
244
+ const r = clientsMod.mergeOpenCodeMcpFile(file, {
245
+ command: 'npx',
246
+ args: ['-y', '@zlnap/astrolabe', '.'],
247
+ });
248
+ assert.equal(r.format, 'v2');
249
+ const doc = clientsMod.parseJsonc(r.text);
250
+ assert.ok(doc.mcp.servers.astrolabe);
251
+ assert.equal(doc.mcp.servers.astrolabe.enabled, undefined);
252
+ assert.ok(doc.mcp.servers.other);
253
+ } finally {
254
+ fs.rmSync(home, { recursive: true, force: true });
255
+ }
256
+ });
257
+
258
+ test('OpenCode resolve prefers existing ~/.opencode/config.json', () => {
259
+ const home = tmpHome();
260
+ try {
261
+ const old = path.join(home, '.opencode', 'config.json');
262
+ fs.mkdirSync(path.dirname(old), { recursive: true });
263
+ fs.writeFileSync(old, '{}\n');
264
+ assert.equal(clientsMod.resolveOpenCodeConfigPath(home, fs.existsSync), old);
265
+ } finally {
266
+ fs.rmSync(home, { recursive: true, force: true });
267
+ }
268
+ });
269
+
270
+ test('OpenCode detect uses config dir markers', () => {
271
+ const home = tmpHome();
272
+ try {
273
+ fs.mkdirSync(path.join(home, '.config', 'opencode'), { recursive: true });
274
+ const d = clientsMod.getClient('opencode').detect(home, { PATH: '/nonexistent' });
275
+ assert.equal(d.detected, true);
276
+ assert.ok(d.evidence.some((e) => e.includes('.config/opencode')));
277
+ } finally {
278
+ fs.rmSync(home, { recursive: true, force: true });
279
+ }
280
+ });
281
+
282
+ test('DSH cordis patch keeps orca-managed block', () => {
283
+ const home = tmpHome();
284
+ try {
285
+ fs.mkdirSync(path.join(home, '.dsh'), { recursive: true });
286
+ const patch = path.join(home, '.dsh', 'cordis.patch.yml');
287
+ fs.writeFileSync(
288
+ patch,
289
+ `# >>> orca-managed-dsh-hooks (managed by Orca; do not edit) >>>
290
+ - insert:
291
+ - id: orca-agent-hooks
292
+ name: '@deepseek-ai/dsh-hooks-claude-code'
293
+ # <<< orca-managed-dsh-hooks <<<
294
+ `,
295
+ );
296
+ const r = clientsMod.mergeDshCordisPatch(patch);
297
+ assert.match(r.text, /orca-managed-dsh-hooks/);
298
+ assert.match(r.text, /astrolabe-managed-dsh-guard/);
299
+ assert.match(r.text, /astrolabe-dsh-guard/);
300
+ } finally {
301
+ fs.rmSync(home, { recursive: true, force: true });
302
+ }
303
+ });
304
+
305
+ test('DSH plan copies vendor plugin and writes patch', () => {
306
+ const home = tmpHome();
307
+ try {
308
+ fs.mkdirSync(path.join(home, '.dsh', 'profiles', 'node_modules'), { recursive: true });
309
+ const client = clientsMod.getClient('deepseek-harness');
310
+ const plan = client.plan({
311
+ home,
312
+ launch: { command: 'npx', args: ['-y', '@zlnap/astrolabe', '.'] },
313
+ });
314
+ const applied = plan.apply();
315
+ assert.equal(applied.skipped, undefined);
316
+ applied.write();
317
+ assert.ok(
318
+ fs.existsSync(
319
+ path.join(home, '.dsh', 'profiles', 'node_modules', 'astrolabe-dsh-guard', 'package.json'),
320
+ ),
321
+ );
322
+ const text = fs.readFileSync(path.join(home, '.dsh', 'cordis.patch.yml'), 'utf8');
323
+ assert.match(text, /astrolabe-managed-dsh-guard/);
324
+ } finally {
325
+ fs.rmSync(home, { recursive: true, force: true });
326
+ }
327
+ });
328
+
329
+
330
+ test('Codebuddy detect + MCP + withHooks settings merge', () => {
331
+ const home = tmpHome();
332
+ try {
333
+ fs.mkdirSync(path.join(home, '.codebuddy'), { recursive: true });
334
+ fs.writeFileSync(
335
+ path.join(home, '.codebuddy', 'settings.json'),
336
+ JSON.stringify(
337
+ {
338
+ hooks: {
339
+ PreToolUse: [{ hooks: [{ type: 'command', command: 'echo keep' }] }],
340
+ },
341
+ },
342
+ null,
343
+ 2,
344
+ ),
345
+ );
346
+ const client = clientsMod.getClient('codebuddy');
347
+ const det = client.detect(home, { PATH: '/nonexistent' });
348
+ assert.equal(det.detected, true);
349
+ const launch = { command: 'npx', args: ['-y', '@zlnap/astrolabe', '.'] };
350
+ const plan = client.plan({ home, launch, withHooks: true });
351
+ const applied = plan.apply();
352
+ assert.equal(applied.skipped, undefined);
353
+ applied.write();
354
+ const mcp = JSON.parse(
355
+ fs.readFileSync(path.join(home, '.codebuddy', '.mcp.json'), 'utf8'),
356
+ );
357
+ assert.equal(mcp.mcpServers.astrolabe.command, 'npx');
358
+ assert.equal(mcp.mcpServers.astrolabe.type, 'stdio');
359
+ const settings = JSON.parse(
360
+ fs.readFileSync(path.join(home, '.codebuddy', 'settings.json'), 'utf8'),
361
+ );
362
+ assert.equal(settings.hooks.PreToolUse[0].hooks[0].command, 'echo keep');
363
+ assert.ok(
364
+ settings.hooks.PreToolUse.some((e) =>
365
+ (e.hooks || []).some((h) => String(h.command).includes('--client=codebuddy')),
366
+ ),
367
+ );
368
+ assert.ok(
369
+ settings.hooks.SessionEnd.some((e) =>
370
+ (e.hooks || []).some((h) => String(h.command).includes('hooks cleanup')),
371
+ ),
372
+ );
373
+ } finally {
374
+ fs.rmSync(home, { recursive: true, force: true });
375
+ }
376
+ });
377
+
378
+ test('Grok MCP toml + withHooks astrolabe.json', () => {
379
+ const home = tmpHome();
380
+ try {
381
+ fs.mkdirSync(path.join(home, '.grok'), { recursive: true });
382
+ fs.writeFileSync(path.join(home, '.grok', 'config.toml'), 'model = "x"\n');
383
+ const client = clientsMod.getClient('grok');
384
+ assert.equal(client.detect(home, { PATH: '/nonexistent' }).detected, true);
385
+ const launch = { command: 'npx', args: ['-y', '@zlnap/astrolabe', '.'] };
386
+ const plan = client.plan({ home, launch, withHooks: true });
387
+ const applied = plan.apply();
388
+ applied.write();
389
+ const toml = fs.readFileSync(path.join(home, '.grok', 'config.toml'), 'utf8');
390
+ assert.match(toml, /model = "x"/);
391
+ assert.match(toml, /\[mcp_servers\.astrolabe\]/);
392
+ assert.match(toml, /command = "npx"/);
393
+ const hooks = JSON.parse(
394
+ fs.readFileSync(path.join(home, '.grok', 'hooks', 'astrolabe.json'), 'utf8'),
395
+ );
396
+ assert.ok(hooks.hooks.PreToolUse);
397
+ assert.ok(hooks.hooks.SessionEnd);
398
+ assert.match(
399
+ hooks.hooks.PreToolUse[0].hooks[0].command,
400
+ /--client=grok/,
401
+ );
402
+ } finally {
403
+ fs.rmSync(home, { recursive: true, force: true });
404
+ }
405
+ });
406
+
407
+ test('Kimi Code mcp.json + managed [[hooks]] toml', () => {
408
+ const home = tmpHome();
409
+ try {
410
+ fs.mkdirSync(path.join(home, '.kimi-code'), { recursive: true });
411
+ fs.writeFileSync(
412
+ path.join(home, '.kimi-code', 'mcp.json'),
413
+ JSON.stringify({ mcpServers: { keep: { command: 'x', args: [] } } }, null, 2),
414
+ );
415
+ fs.writeFileSync(
416
+ path.join(home, '.kimi-code', 'config.toml'),
417
+ 'default_model = "kimi"\n\n[[hooks]]\nevent = "Notification"\ncommand = "echo keep"\n',
418
+ );
419
+ const client = clientsMod.getClient('kimi');
420
+ assert.equal(client.id, 'kimicode');
421
+ const launch = { command: 'npx', args: ['-y', '@zlnap/astrolabe', '.'] };
422
+ const plan = client.plan({ home, launch, withHooks: true });
423
+ plan.apply().write();
424
+ const mcp = JSON.parse(fs.readFileSync(path.join(home, '.kimi-code', 'mcp.json'), 'utf8'));
425
+ assert.equal(mcp.mcpServers.keep.command, 'x');
426
+ assert.equal(mcp.mcpServers.astrolabe.command, 'npx');
427
+ const toml = fs.readFileSync(path.join(home, '.kimi-code', 'config.toml'), 'utf8');
428
+ assert.match(toml, /default_model = "kimi"/);
429
+ assert.match(toml, /echo keep/);
430
+ assert.match(toml, /astrolabe-managed-kimi-hooks/);
431
+ assert.match(toml, /--client=kimicode/);
432
+ assert.match(toml, /event = "SessionEnd"/);
433
+ // idempotent
434
+ const again = clientsMod.mergeKimiHooksToml(path.join(home, '.kimi-code', 'config.toml'));
435
+ assert.equal(again.replaced, true);
436
+ assert.equal((again.text.match(/astrolabe-managed-kimi-hooks/g) || []).length, 2);
437
+ } finally {
438
+ fs.rmSync(home, { recursive: true, force: true });
439
+ }
440
+ });
441
+
442
+ test('ZCode mcp.servers + hooks.events without SessionEnd', () => {
443
+ const home = tmpHome();
444
+ try {
445
+ const cli = path.join(home, '.zcode', 'cli');
446
+ fs.mkdirSync(cli, { recursive: true });
447
+ fs.writeFileSync(
448
+ path.join(cli, 'config.json'),
449
+ JSON.stringify(
450
+ {
451
+ mcp: { servers: { keep: { type: 'stdio', command: 'x', args: [] } } },
452
+ hooks: {
453
+ enabled: false,
454
+ events: {
455
+ SessionEnd: [{ hooks: [{ type: 'command', command: 'bad' }] }],
456
+ PreToolUse: [{ hooks: [{ type: 'command', command: 'echo keep' }] }],
457
+ },
458
+ },
459
+ },
460
+ null,
461
+ 2,
462
+ ),
463
+ );
464
+ const client = clientsMod.getClient('zai');
465
+ assert.equal(client.id, 'zcode');
466
+ const launch = { command: 'npx', args: ['-y', '@zlnap/astrolabe', '.'] };
467
+ const plan = client.plan({ home, launch, withHooks: true });
468
+ plan.apply().write();
469
+ const doc = JSON.parse(fs.readFileSync(path.join(cli, 'config.json'), 'utf8'));
470
+ assert.equal(doc.mcp.servers.keep.command, 'x');
471
+ assert.equal(doc.mcp.servers.astrolabe.type, 'stdio');
472
+ assert.equal(doc.mcp.servers.astrolabe.command, 'npx');
473
+ assert.equal(doc.hooks.enabled, true);
474
+ assert.equal(doc.hooks.events.SessionEnd, undefined);
475
+ assert.ok(
476
+ !JSON.stringify(doc.hooks).includes('SessionEnd'),
477
+ 'SessionEnd must not appear in ZCode hooks',
478
+ );
479
+ assert.ok(
480
+ doc.hooks.events.PreToolUse.some((e) =>
481
+ (e.hooks || []).some((h) => String(h.command).includes('--client=zcode')),
482
+ ),
483
+ );
484
+ assert.ok(
485
+ doc.hooks.events.PreToolUse.some((e) =>
486
+ (e.hooks || []).some((h) => h.command === 'echo keep'),
487
+ ),
488
+ );
489
+ // MCP-only path does not force hooks.enabled
490
+ const home2 = tmpHome();
491
+ try {
492
+ fs.mkdirSync(path.join(home2, '.zcode', 'cli'), { recursive: true });
493
+ const p2 = clientsMod.getClient('zcode').plan({
494
+ home: home2,
495
+ launch,
496
+ withHooks: false,
497
+ });
498
+ p2.apply().write();
499
+ const d2 = JSON.parse(
500
+ fs.readFileSync(path.join(home2, '.zcode', 'cli', 'config.json'), 'utf8'),
501
+ );
502
+ assert.ok(d2.mcp.servers.astrolabe);
503
+ assert.equal(d2.hooks, undefined);
504
+ } finally {
505
+ fs.rmSync(home2, { recursive: true, force: true });
506
+ }
507
+ } finally {
508
+ fs.rmSync(home, { recursive: true, force: true });
509
+ }
510
+ });
511
+
512
+ test('setup --clients aliases resolve for new clients', () => {
513
+ const home = tmpHome();
514
+ try {
515
+ fs.mkdirSync(path.join(home, '.grok'), { recursive: true });
516
+ const dry = spawnSync(
517
+ process.execPath,
518
+ [
519
+ BIN,
520
+ 'setup',
521
+ '--yes',
522
+ '--clients=grokbuild,kimi,zai,codebuddy',
523
+ '--dry-run',
524
+ '--home',
525
+ home,
526
+ '--no-cursor-permissions',
527
+ ],
528
+ { encoding: 'utf8' },
529
+ );
530
+ assert.equal(dry.status, 0, dry.stderr + dry.stdout);
531
+ assert.match(dry.stdout, /Grok Build/);
532
+ assert.match(dry.stdout, /Kimi Code/);
533
+ assert.match(dry.stdout, /ZCode/);
534
+ // codebuddy skipped (no ~/.codebuddy)
535
+ assert.match(dry.stdout, /Codebuddy: SKIP|Codebuddy/);
536
+ } finally {
537
+ fs.rmSync(home, { recursive: true, force: true });
538
+ }
539
+ });
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@zlnap/astrolabe",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.17.0",
4
+ "mcpName": "io.github.wojiushirencai/astrolabe",
5
+ "description": "Local MCP server for code search and navigation (search_code, find_symbol, find_references, goto_definition) for AI coding agents. Node shim for the native Astrolabe AI binary.",
6
+ "keywords": [
7
+ "mcp",
8
+ "mcp-server",
9
+ "model-context-protocol",
10
+ "code-search",
11
+ "code-navigation",
12
+ "code-intelligence",
13
+ "claude-code",
14
+ "codex",
15
+ "cursor",
16
+ "tree-sitter",
17
+ "lsp",
18
+ "ai-coding",
19
+ "coding-agent"
20
+ ],
21
+ "license": "MIT OR Apache-2.0",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/wojiushirencai/Astrolabe.git",
25
+ "directory": "npm/packages/astrolabe"
26
+ },
27
+ "homepage": "https://github.com/wojiushirencai/Astrolabe",
28
+ "bugs": {
29
+ "url": "https://github.com/wojiushirencai/Astrolabe/issues"
30
+ },
31
+ "bin": {
32
+ "astrolabe": "bin/astrolabe.js"
33
+ },
34
+ "files": [
35
+ "bin",
36
+ "lib",
37
+ "README.md"
38
+ ],
39
+ "engines": {
40
+ "node": ">=18"
41
+ },
42
+ "optionalDependencies": {
43
+ "@zlnap/darwin-arm64": "0.17.0",
44
+ "@zlnap/darwin-x64": "0.17.0",
45
+ "@zlnap/linux-x64-gnu": "0.17.0",
46
+ "@zlnap/linux-x64-musl": "0.17.0",
47
+ "@zlnap/linux-arm64-gnu": "0.17.0",
48
+ "@zlnap/win32-x64-msvc": "0.17.0"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }