@wenrwa/marketplace-cli 0.7.3 → 0.7.5
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/dist/__tests__/bridge-browser-pointer.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-pointer.test.js +135 -0
- package/dist/__tests__/bridge-launch-supervisor.test.d.ts +1 -0
- package/dist/__tests__/bridge-launch-supervisor.test.js +186 -0
- package/dist/bridge/contextWriter.js +3 -0
- package/dist/bridge/launchSupervisor.d.ts +61 -0
- package/dist/bridge/launchSupervisor.js +157 -0
- package/dist/bridge/localContextWriter.js +3 -0
- package/dist/bridge/sessionManager.d.ts +6 -0
- package/dist/bridge/sessionManager.js +9 -1
- package/dist/bridge/workGraphPointer.d.ts +16 -0
- package/dist/bridge/workGraphPointer.js +21 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
/**
|
|
37
|
+
* cvt-09 — the managed region tells agents WHEN to call `show_in_browser`.
|
|
38
|
+
* Both lanes: hosted (contextWriter → fenced region of CLAUDE.md) and local
|
|
39
|
+
* (localContextWriter → bridge-owned .hiveop/context.md). Asserts on runtime
|
|
40
|
+
* output, never source text.
|
|
41
|
+
*/
|
|
42
|
+
const fs = __importStar(require("fs"));
|
|
43
|
+
const os = __importStar(require("os"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const contextWriter_1 = require("../bridge/contextWriter");
|
|
46
|
+
const localContextWriter_1 = require("../bridge/localContextWriter");
|
|
47
|
+
const workGraphPointer_1 = require("../bridge/workGraphPointer");
|
|
48
|
+
const LINE = 'Your pilot watches a Browser pane on their HiveOp canvas. After you start a dev server or finish a deploy, confirm the URL is live (fetch it), then call `show_in_browser` with that URL. It reloads their tab if open, otherwise opens it in a background tab — it never touches their other tabs.';
|
|
49
|
+
const count = (hay, needle) => hay.split(needle).length - 1;
|
|
50
|
+
describe('BROWSER_POINTER (cvt-09)', () => {
|
|
51
|
+
it('is the heading plus the exact line, ≤3 lines', () => {
|
|
52
|
+
expect(workGraphPointer_1.BROWSER_POINTER).toBe(`## Browser\n${LINE}`);
|
|
53
|
+
expect(workGraphPointer_1.BROWSER_POINTER.split('\n').length).toBeLessThanOrEqual(3);
|
|
54
|
+
});
|
|
55
|
+
it('is silent when the MCP tools are not registered', () => {
|
|
56
|
+
expect((0, workGraphPointer_1.browserSection)({ workGraphAvailable: false })).toBeNull();
|
|
57
|
+
expect((0, workGraphPointer_1.browserSection)({})).toBeNull();
|
|
58
|
+
expect((0, workGraphPointer_1.browserSection)({ workGraphAvailable: true })).toBe(workGraphPointer_1.BROWSER_POINTER);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('hosted lane — CLAUDE.md fenced region', () => {
|
|
62
|
+
const CLAUDE = '/data/repos/ws-7/CLAUDE.md';
|
|
63
|
+
const makeFs = (seed) => {
|
|
64
|
+
const files = new Map(Object.entries(seed));
|
|
65
|
+
return {
|
|
66
|
+
files,
|
|
67
|
+
deps: {
|
|
68
|
+
reposBaseDir: '/data/repos',
|
|
69
|
+
exists: (p) => files.has(p),
|
|
70
|
+
readFile: (p) => files.get(p) ?? '',
|
|
71
|
+
writeFile: (p, c) => { files.set(p, c); },
|
|
72
|
+
mkdirp: () => { },
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
it('carries the line exactly once inside the pointer markers after two writes; user content untouched', () => {
|
|
77
|
+
const userTop = '# My project\n\nHand-written rules.\n\n';
|
|
78
|
+
const userBottom = '\n\n## Mine\nkeep me\n';
|
|
79
|
+
const { files, deps } = makeFs({ [CLAUDE]: userTop + userBottom });
|
|
80
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-7', content: 'pushed', workGraphAvailable: true, memoryAvailable: true }, deps);
|
|
81
|
+
const first = files.get(CLAUDE);
|
|
82
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-7', pointersOnly: true, workGraphAvailable: true, memoryAvailable: true }, deps);
|
|
83
|
+
const out = files.get(CLAUDE);
|
|
84
|
+
expect(out).toBe(first);
|
|
85
|
+
expect(count(out, LINE)).toBe(1);
|
|
86
|
+
expect(count(out, '## Browser')).toBe(1);
|
|
87
|
+
const ps = out.indexOf(contextWriter_1.POINTERS_START_MARKER);
|
|
88
|
+
const pe = out.indexOf(contextWriter_1.POINTERS_END_MARKER);
|
|
89
|
+
const at = out.indexOf(LINE);
|
|
90
|
+
expect(ps).toBeGreaterThan(-1);
|
|
91
|
+
expect(at).toBeGreaterThan(ps);
|
|
92
|
+
expect(at).toBeLessThan(pe);
|
|
93
|
+
const rs = out.indexOf(contextWriter_1.CONTEXT_START_MARKER);
|
|
94
|
+
const re = out.indexOf(contextWriter_1.CONTEXT_END_MARKER) + contextWriter_1.CONTEXT_END_MARKER.length;
|
|
95
|
+
const outside = out.slice(0, rs) + out.slice(re);
|
|
96
|
+
expect(outside).toContain('# My project\n\nHand-written rules.');
|
|
97
|
+
expect(outside).toContain('## Mine\nkeep me');
|
|
98
|
+
expect(outside).not.toContain('show_in_browser');
|
|
99
|
+
});
|
|
100
|
+
it('carries the line in a match workspace too', () => {
|
|
101
|
+
const { files, deps } = makeFs({});
|
|
102
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-7', content: '', workGraphAvailable: true, match: true }, deps);
|
|
103
|
+
expect(count(files.get(CLAUDE), LINE)).toBe(1);
|
|
104
|
+
});
|
|
105
|
+
it('omits it when the MCP tools are not registered', () => {
|
|
106
|
+
const { files, deps } = makeFs({});
|
|
107
|
+
(0, contextWriter_1.writeContext)({ workspaceId: 'ws-7', content: 'pushed' }, deps);
|
|
108
|
+
expect(files.get(CLAUDE)).not.toContain('show_in_browser');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('local lane — .hiveop/context.md', () => {
|
|
112
|
+
let projectRoot;
|
|
113
|
+
beforeEach(() => { projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'cvt09-')); });
|
|
114
|
+
afterEach(() => { fs.rmSync(projectRoot, { recursive: true, force: true }); });
|
|
115
|
+
const deps = () => ({
|
|
116
|
+
exists: (p) => fs.existsSync(p),
|
|
117
|
+
readFile: (p) => fs.readFileSync(p, 'utf-8'),
|
|
118
|
+
mkdirp: (d) => fs.mkdirSync(d, { recursive: true }),
|
|
119
|
+
writeFileAtomic: (p, c) => fs.writeFileSync(p, c, 'utf-8'),
|
|
120
|
+
remove: (p) => fs.rmSync(p, { force: true }),
|
|
121
|
+
});
|
|
122
|
+
it('carries the line exactly once inside the pointer markers after two writes; CLAUDE.md untouched', () => {
|
|
123
|
+
const claudeMd = path.join(projectRoot, 'CLAUDE.md');
|
|
124
|
+
fs.writeFileSync(claudeMd, '# user rules\n');
|
|
125
|
+
(0, localContextWriter_1.writeLocalContext)({ projectRoot, content: '## files\n- a.ts', workGraphAvailable: true, memoryAvailable: true }, deps());
|
|
126
|
+
(0, localContextWriter_1.writeLocalContext)({ projectRoot, pointersOnly: true, workGraphAvailable: true, memoryAvailable: true }, deps());
|
|
127
|
+
const out = fs.readFileSync(path.join(projectRoot, localContextWriter_1.LOCAL_CONTEXT_RELATIVE_PATH), 'utf-8');
|
|
128
|
+
expect(count(out, LINE)).toBe(1);
|
|
129
|
+
const at = out.indexOf(LINE);
|
|
130
|
+
expect(at).toBeGreaterThan(out.indexOf(contextWriter_1.POINTERS_START_MARKER));
|
|
131
|
+
expect(at).toBeLessThan(out.indexOf(contextWriter_1.POINTERS_END_MARKER));
|
|
132
|
+
expect(out).toContain('## files\n- a.ts');
|
|
133
|
+
expect(fs.readFileSync(claudeMd, 'utf-8')).toBe('# user rules\n');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const launchSupervisor_1 = require("../bridge/launchSupervisor");
|
|
4
|
+
const sessionManager_1 = require("../bridge/sessionManager");
|
|
5
|
+
const ID = '445be432-0000-4000-8000-000000000001';
|
|
6
|
+
// Verbatim shapes from Claude Code 2.1.289 (stderr is red-wrapped; the resume
|
|
7
|
+
// miss is drawn by the TUI with a truecolor escape).
|
|
8
|
+
const IN_USE = `\x1b[31mError: Session ID ${ID} is already in use.\x1b[39m\r\n`;
|
|
9
|
+
const NO_CONVO = `\x1b[?2026h\x1b[38;2;255;107;128mNo conversation found with session ID: ${ID}\x1b[39m\r\r\n`;
|
|
10
|
+
class FakePty {
|
|
11
|
+
args;
|
|
12
|
+
written = [];
|
|
13
|
+
killed = false;
|
|
14
|
+
dataCb = null;
|
|
15
|
+
exitCb = null;
|
|
16
|
+
constructor(args) {
|
|
17
|
+
this.args = args;
|
|
18
|
+
}
|
|
19
|
+
write(d) { this.written.push(d); }
|
|
20
|
+
resize() { }
|
|
21
|
+
kill() { this.killed = true; }
|
|
22
|
+
onData(cb) { this.dataCb = cb; }
|
|
23
|
+
onExit(cb) { this.exitCb = cb; }
|
|
24
|
+
emit(d) { this.dataCb?.(d); }
|
|
25
|
+
exit(code) { this.exitCb?.(code); }
|
|
26
|
+
}
|
|
27
|
+
function harness(args) {
|
|
28
|
+
const spawned = [];
|
|
29
|
+
const factory = {
|
|
30
|
+
spawn: (o) => {
|
|
31
|
+
const p = new FakePty(o.args ?? []);
|
|
32
|
+
spawned.push(p);
|
|
33
|
+
return p;
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
let t = 0;
|
|
37
|
+
const logs = [];
|
|
38
|
+
const pty = (0, launchSupervisor_1.superviseLaunch)(factory, { cols: 80, rows: 24, command: 'claude', args }, { now: () => t, log: (m) => logs.push(m), settle: (fn) => fn(), windowMs: 10_000 });
|
|
39
|
+
const out = [];
|
|
40
|
+
const exits = [];
|
|
41
|
+
pty.onData((d) => out.push(d));
|
|
42
|
+
pty.onExit((c) => exits.push(c));
|
|
43
|
+
return { pty, spawned, out, exits, logs, advance: (ms) => { t += ms; } };
|
|
44
|
+
}
|
|
45
|
+
describe('launchSupervisor — pure helpers', () => {
|
|
46
|
+
it('session-in-use on --session-id → swap to --resume, keeping every other arg', () => {
|
|
47
|
+
expect((0, launchSupervisor_1.retryArgsFor)(['--dangerously-skip-permissions', '--settings', '/s.json', '--session-id', ID], IN_USE))
|
|
48
|
+
.toEqual(['--dangerously-skip-permissions', '--settings', '/s.json', '--resume', ID]);
|
|
49
|
+
});
|
|
50
|
+
it('no-conversation on --resume → swap to --session-id', () => {
|
|
51
|
+
expect((0, launchSupervisor_1.retryArgsFor)(['--resume', ID], NO_CONVO)).toEqual(['--session-id', ID]);
|
|
52
|
+
});
|
|
53
|
+
it('no session flag, or an unrelated error → no retry', () => {
|
|
54
|
+
expect((0, launchSupervisor_1.retryArgsFor)([], IN_USE)).toBeNull();
|
|
55
|
+
expect((0, launchSupervisor_1.retryArgsFor)(['--session-id', ID], 'Error: Invalid session ID. Must be a valid UUID.')).toBeNull();
|
|
56
|
+
expect((0, launchSupervisor_1.retryArgsFor)(['--resume', ID], IN_USE)).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
it('firstMeaningfulLine strips ANSI, "Error:" and stack frames', () => {
|
|
59
|
+
expect((0, launchSupervisor_1.firstMeaningfulLine)(IN_USE)).toBe(`Session ID ${ID} is already in use`);
|
|
60
|
+
expect((0, launchSupervisor_1.firstMeaningfulLine)('\r\n\x1b[1m\x1b[0m\r\nError: boom.\n at foo (x.js:1:1)\n')).toBe('boom');
|
|
61
|
+
expect((0, launchSupervisor_1.firstMeaningfulLine)('\x1b[?25h\r\n')).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
it('friendly line is one short sentence with no stack trace', () => {
|
|
64
|
+
const line = (0, launchSupervisor_1.friendlyStartFailure)('Error: kaput\n at a (b.js:1:1)\n at c (d.js:2:2)', 1);
|
|
65
|
+
expect(line).toContain("Claude couldn't start: kaput. Reopen the pane to try again.");
|
|
66
|
+
expect(line).not.toContain('at a (');
|
|
67
|
+
expect((0, launchSupervisor_1.friendlyStartFailure)('', 3)).toContain("Claude couldn't start: it exited with code 3.");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('launchSupervisor — supervised PTY', () => {
|
|
71
|
+
it('session-in-use → exactly one relaunch with --resume; the healthy retry is passed through', () => {
|
|
72
|
+
const h = harness(['--session-id', ID]);
|
|
73
|
+
h.spawned[0].emit(IN_USE);
|
|
74
|
+
h.advance(800);
|
|
75
|
+
h.spawned[0].exit(1);
|
|
76
|
+
expect(h.spawned).toHaveLength(2);
|
|
77
|
+
expect(h.spawned[1].args).toEqual(['--resume', ID]);
|
|
78
|
+
expect(h.exits).toEqual([]); // the pane never sees the first exit
|
|
79
|
+
expect(h.out.join('')).not.toContain("couldn't start");
|
|
80
|
+
h.pty.write('hi');
|
|
81
|
+
expect(h.spawned[1].written).toEqual(['hi']);
|
|
82
|
+
h.spawned[1].emit('welcome back');
|
|
83
|
+
expect(h.out[h.out.length - 1]).toBe('welcome back');
|
|
84
|
+
expect(h.logs.join('\n')).toMatch(/already in use/);
|
|
85
|
+
});
|
|
86
|
+
it('resume-missing → exactly one relaunch with --session-id', () => {
|
|
87
|
+
const h = harness(['--dangerously-skip-permissions', '--resume', ID]);
|
|
88
|
+
h.spawned[0].emit(NO_CONVO);
|
|
89
|
+
h.spawned[0].exit(1);
|
|
90
|
+
expect(h.spawned).toHaveLength(2);
|
|
91
|
+
expect(h.spawned[1].args).toEqual(['--dangerously-skip-permissions', '--session-id', ID]);
|
|
92
|
+
expect(h.exits).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
it('never loops: a retry that fails the same way gets the friendly line and the exit, no third spawn', () => {
|
|
95
|
+
const h = harness(['--session-id', ID]);
|
|
96
|
+
h.spawned[0].emit(IN_USE);
|
|
97
|
+
h.spawned[0].exit(1);
|
|
98
|
+
h.spawned[1].emit(NO_CONVO);
|
|
99
|
+
h.spawned[1].exit(1);
|
|
100
|
+
expect(h.spawned).toHaveLength(2);
|
|
101
|
+
expect(h.exits).toEqual([1]);
|
|
102
|
+
expect(h.out.join('')).toContain(`Claude couldn't start: No conversation found with session ID: ${ID}. Reopen the pane to try again.`);
|
|
103
|
+
});
|
|
104
|
+
it('generic fast non-zero exit → friendly line, then the exit; raw error kept in the log', () => {
|
|
105
|
+
const h = harness(['--session-id', ID]);
|
|
106
|
+
h.spawned[0].emit('\x1b[31mError: Invalid session ID. Must be a valid UUID.\x1b[39m\r\n');
|
|
107
|
+
h.advance(300);
|
|
108
|
+
h.spawned[0].exit(1);
|
|
109
|
+
expect(h.spawned).toHaveLength(1);
|
|
110
|
+
expect(h.exits).toEqual([1]);
|
|
111
|
+
const last = h.out[h.out.length - 1];
|
|
112
|
+
expect(last).toContain("Claude couldn't start: Invalid session ID. Must be a valid UUID. Reopen the pane to try again.");
|
|
113
|
+
expect(h.logs.join('\n')).toContain('Error: Invalid session ID. Must be a valid UUID.');
|
|
114
|
+
});
|
|
115
|
+
it('a long-running session that exits later is untouched — no retry, no friendly line', () => {
|
|
116
|
+
const h = harness(['--session-id', ID]);
|
|
117
|
+
h.spawned[0].emit('working...');
|
|
118
|
+
h.advance(60_000);
|
|
119
|
+
h.spawned[0].emit(IN_USE); // even matching text, outside the window
|
|
120
|
+
h.spawned[0].exit(1);
|
|
121
|
+
expect(h.spawned).toHaveLength(1);
|
|
122
|
+
expect(h.exits).toEqual([1]);
|
|
123
|
+
expect(h.out.join('')).not.toContain("couldn't start");
|
|
124
|
+
expect(h.logs).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
it('a fast clean exit (code 0) passes straight through', () => {
|
|
127
|
+
const h = harness(['--resume', ID]);
|
|
128
|
+
h.spawned[0].exit(0);
|
|
129
|
+
expect(h.spawned).toHaveLength(1);
|
|
130
|
+
expect(h.exits).toEqual([0]);
|
|
131
|
+
expect(h.out).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
it('a bridge kill is never retried or annotated', () => {
|
|
134
|
+
const h = harness(['--session-id', ID]);
|
|
135
|
+
h.spawned[0].emit(IN_USE);
|
|
136
|
+
h.pty.kill();
|
|
137
|
+
expect(h.spawned[0].killed).toBe(true);
|
|
138
|
+
h.spawned[0].exit(1);
|
|
139
|
+
expect(h.spawned).toHaveLength(1);
|
|
140
|
+
expect(h.exits).toEqual([1]);
|
|
141
|
+
expect(h.out.join('')).not.toContain("couldn't start");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
describe('launchSupervisor — wired through SessionManager', () => {
|
|
145
|
+
const fakeSnapshot = () => ({
|
|
146
|
+
write: () => Promise.resolve(),
|
|
147
|
+
resize: () => { },
|
|
148
|
+
serialize: () => Promise.resolve(''),
|
|
149
|
+
dispose: () => { },
|
|
150
|
+
});
|
|
151
|
+
function manager() {
|
|
152
|
+
const spawned = [];
|
|
153
|
+
const factory = { spawn: (o) => { const p = new FakePty(o.args ?? []); spawned.push(p); return p; } };
|
|
154
|
+
const mgr = new sessionManager_1.SessionManager(factory, {
|
|
155
|
+
snapshotFactory: fakeSnapshot,
|
|
156
|
+
launchSupervisor: { now: () => 0, log: () => { }, settle: (fn) => fn() },
|
|
157
|
+
});
|
|
158
|
+
const exits = [];
|
|
159
|
+
mgr.setOnSessionExit((id, code) => exits.push([id, code]));
|
|
160
|
+
return { mgr, spawned, exits };
|
|
161
|
+
}
|
|
162
|
+
it('a claude launch self-heals inside one session: same id, one exit only when the retry dies', async () => {
|
|
163
|
+
const { mgr, spawned, exits } = manager();
|
|
164
|
+
const id = mgr.open({ cols: 80, rows: 24, id: ID, command: 'claude', args: ['--session-id', ID] });
|
|
165
|
+
const seen = [];
|
|
166
|
+
await mgr.attach(id, (f) => { if (f.t === 'data')
|
|
167
|
+
seen.push(f.data); });
|
|
168
|
+
spawned[0].emit(IN_USE);
|
|
169
|
+
spawned[0].exit(1);
|
|
170
|
+
expect(spawned).toHaveLength(2);
|
|
171
|
+
expect(mgr.list()).toEqual([ID]);
|
|
172
|
+
expect(exits).toEqual([]);
|
|
173
|
+
spawned[1].emit('boom\r\n');
|
|
174
|
+
spawned[1].exit(2);
|
|
175
|
+
expect(exits).toEqual([[ID, 2]]);
|
|
176
|
+
expect(seen.join('')).toContain("Claude couldn't start: boom. Reopen the pane to try again.");
|
|
177
|
+
});
|
|
178
|
+
it('a shell launch (no command) is not supervised', () => {
|
|
179
|
+
const { mgr, spawned, exits } = manager();
|
|
180
|
+
mgr.open({ cols: 80, rows: 24 });
|
|
181
|
+
spawned[0].emit('Error: nope\r\n');
|
|
182
|
+
spawned[0].exit(1);
|
|
183
|
+
expect(spawned).toHaveLength(1);
|
|
184
|
+
expect(exits).toHaveLength(1);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -153,6 +153,9 @@ function writeContext(input, deps) {
|
|
|
153
153
|
pointers.push(work);
|
|
154
154
|
if (input.memoryAvailable)
|
|
155
155
|
pointers.push((0, memoryPointer_1.memorySection)(input.memoryBrief));
|
|
156
|
+
const browser = (0, workGraphPointer_1.browserSection)(input);
|
|
157
|
+
if (browser)
|
|
158
|
+
pointers.push(browser);
|
|
156
159
|
// wmr-07: a pointers-only refresh keeps the region's existing file content
|
|
157
160
|
// rather than replacing it with the (absent) `content` argument.
|
|
158
161
|
const fileContent = input.pointersOnly
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launch supervisor — makes a `claude` that dies at startup either heal
|
|
3
|
+
* itself or say so in plain words, instead of leaving its raw stderr as the
|
|
4
|
+
* last thing in the pane.
|
|
5
|
+
*
|
|
6
|
+
* Wraps ONE agent launch (SessionManager.open with `command: 'claude'`) in a
|
|
7
|
+
* PtyLike that the session manager cannot tell apart from a plain PTY. Inside,
|
|
8
|
+
* it watches the first `windowMs` of the process's life:
|
|
9
|
+
*
|
|
10
|
+
* - `--session-id <id>` exits early with "Session ID <id> is already in use"
|
|
11
|
+
* → the conversation already exists (e.g. it was recorded under a project
|
|
12
|
+
* dir the existence check missed), so relaunch ONCE with `--resume <id>`.
|
|
13
|
+
* - `--resume <id>` exits early with "No conversation found with session ID"
|
|
14
|
+
* → there is nothing to resume, so relaunch ONCE with `--session-id <id>`.
|
|
15
|
+
* - any other early non-zero exit (or a retry that also fails) → write one
|
|
16
|
+
* short line into the stream ("Claude couldn't start: …") and let the exit
|
|
17
|
+
* through. The raw output goes to the bridge log, not into a second copy.
|
|
18
|
+
*
|
|
19
|
+
* At most one retry per open — the retry itself is never retried, so no loop.
|
|
20
|
+
* A process that outlives the window, exits 0, or was killed by the bridge is
|
|
21
|
+
* passed through untouched.
|
|
22
|
+
*
|
|
23
|
+
* Wording checked against Claude Code 2.1.289: the session-in-use error is
|
|
24
|
+
* `Error: Session ID <uuid> is already in use.` (stderr); the resume miss is
|
|
25
|
+
* `No conversation found with session ID: <uuid>` (rendered by the TUI).
|
|
26
|
+
* Both arrive on the PTY, so matching the combined output catches both.
|
|
27
|
+
*
|
|
28
|
+
* Both lanes: the local bridge (loopback + crew relay on a laptop) spawns
|
|
29
|
+
* `claude` through here. The hosted container does NOT — it spawns $SHELL and
|
|
30
|
+
* infra/terminal-image/autolaunch.sh runs `claude`, so the same rules live
|
|
31
|
+
* there too.
|
|
32
|
+
*/
|
|
33
|
+
import { PtyFactory, PtyLike } from './sessionManager';
|
|
34
|
+
/** How long after launch an exit still counts as "couldn't start". */
|
|
35
|
+
export declare const DEFAULT_LAUNCH_WINDOW_MS = 10000;
|
|
36
|
+
export declare const SESSION_IN_USE_RE: RegExp;
|
|
37
|
+
export declare const RESUME_MISSING_RE: RegExp;
|
|
38
|
+
export declare function stripAnsi(s: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* The first line of `output` worth showing a person: ANSI stripped, blank
|
|
41
|
+
* lines and stack frames skipped, a leading "Error:" dropped, trailing period
|
|
42
|
+
* dropped (the caller adds its own punctuation), clipped to ~160 chars.
|
|
43
|
+
*/
|
|
44
|
+
export declare function firstMeaningfulLine(output: string): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Pure decision for an EARLY exit (caller has already checked the window and
|
|
47
|
+
* the retry budget): the argv to relaunch with, or null for "don't retry".
|
|
48
|
+
*/
|
|
49
|
+
export declare function retryArgsFor(args: string[], output: string): string[] | null;
|
|
50
|
+
export declare function friendlyStartFailure(output: string, code: number): string;
|
|
51
|
+
export interface LaunchSupervisorOptions {
|
|
52
|
+
windowMs?: number;
|
|
53
|
+
now?: () => number;
|
|
54
|
+
log?: (msg: string) => void;
|
|
55
|
+
/** node-pty can report exit before the last output chunk is read; an early
|
|
56
|
+
* failure is judged after this short settle. Tests pass a synchronous one. */
|
|
57
|
+
settle?: (fn: () => void) => void;
|
|
58
|
+
}
|
|
59
|
+
type SpawnOpts = Parameters<PtyFactory['spawn']>[0];
|
|
60
|
+
export declare function superviseLaunch(factory: PtyFactory, spawnOpts: SpawnOpts, opts?: LaunchSupervisorOptions): PtyLike;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RESUME_MISSING_RE = exports.SESSION_IN_USE_RE = exports.DEFAULT_LAUNCH_WINDOW_MS = void 0;
|
|
4
|
+
exports.stripAnsi = stripAnsi;
|
|
5
|
+
exports.firstMeaningfulLine = firstMeaningfulLine;
|
|
6
|
+
exports.retryArgsFor = retryArgsFor;
|
|
7
|
+
exports.friendlyStartFailure = friendlyStartFailure;
|
|
8
|
+
exports.superviseLaunch = superviseLaunch;
|
|
9
|
+
/** How long after launch an exit still counts as "couldn't start". */
|
|
10
|
+
exports.DEFAULT_LAUNCH_WINDOW_MS = 10_000;
|
|
11
|
+
/** Cap on how much early output is kept for matching/logging. */
|
|
12
|
+
const MAX_CAPTURE = 16 * 1024;
|
|
13
|
+
exports.SESSION_IN_USE_RE = /Session ID \S+ is already in use/i;
|
|
14
|
+
exports.RESUME_MISSING_RE = /No conversation found/i;
|
|
15
|
+
// CSI, OSC (BEL- or ST-terminated), and lone two-char escapes.
|
|
16
|
+
// eslint-disable-next-line no-control-regex
|
|
17
|
+
const ANSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[@-Z\\-_()#%][0-9A-Za-z]?/g;
|
|
18
|
+
function stripAnsi(s) {
|
|
19
|
+
// eslint-disable-next-line no-control-regex
|
|
20
|
+
return s.replace(ANSI_RE, '').replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The first line of `output` worth showing a person: ANSI stripped, blank
|
|
24
|
+
* lines and stack frames skipped, a leading "Error:" dropped, trailing period
|
|
25
|
+
* dropped (the caller adds its own punctuation), clipped to ~160 chars.
|
|
26
|
+
*/
|
|
27
|
+
function firstMeaningfulLine(output) {
|
|
28
|
+
const lines = stripAnsi(output).split(/\r?\n|\r/);
|
|
29
|
+
for (const raw of lines) {
|
|
30
|
+
const line = raw.trim();
|
|
31
|
+
if (!line)
|
|
32
|
+
continue;
|
|
33
|
+
if (/^at\s+\S/.test(line))
|
|
34
|
+
continue;
|
|
35
|
+
if (/^[-─━═_=*·•\s]+$/.test(line))
|
|
36
|
+
continue;
|
|
37
|
+
let msg = line.replace(/^(?:error|fatal)\s*:\s*/i, '').replace(/[.\s]+$/, '');
|
|
38
|
+
if (!msg)
|
|
39
|
+
continue;
|
|
40
|
+
if (msg.length > 160)
|
|
41
|
+
msg = `${msg.slice(0, 157)}...`;
|
|
42
|
+
return msg;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/** Where `--resume`/`--session-id` sits in argv, or null. */
|
|
47
|
+
function sessionFlagIndex(args) {
|
|
48
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
49
|
+
if (args[i] === '--resume' || args[i] === '--session-id')
|
|
50
|
+
return i;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Pure decision for an EARLY exit (caller has already checked the window and
|
|
56
|
+
* the retry budget): the argv to relaunch with, or null for "don't retry".
|
|
57
|
+
*/
|
|
58
|
+
function retryArgsFor(args, output) {
|
|
59
|
+
const i = sessionFlagIndex(args);
|
|
60
|
+
if (i === null)
|
|
61
|
+
return null;
|
|
62
|
+
const text = stripAnsi(output);
|
|
63
|
+
const flag = args[i];
|
|
64
|
+
if (flag === '--session-id' && exports.SESSION_IN_USE_RE.test(text)) {
|
|
65
|
+
const next = [...args];
|
|
66
|
+
next[i] = '--resume';
|
|
67
|
+
return next;
|
|
68
|
+
}
|
|
69
|
+
if (flag === '--resume' && exports.RESUME_MISSING_RE.test(text)) {
|
|
70
|
+
const next = [...args];
|
|
71
|
+
next[i] = '--session-id';
|
|
72
|
+
return next;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
function friendlyStartFailure(output, code) {
|
|
77
|
+
const reason = firstMeaningfulLine(output) ?? `it exited with code ${code}`;
|
|
78
|
+
return `\r\n\x1b[33mClaude couldn't start: ${reason}. Reopen the pane to try again.\x1b[0m\r\n`;
|
|
79
|
+
}
|
|
80
|
+
/** Clears the pane before the retry so the raw error from the first attempt
|
|
81
|
+
* doesn't sit above the healthy session. */
|
|
82
|
+
const RETRY_CLEAR = '\x1b[2J\x1b[3J\x1b[H';
|
|
83
|
+
function superviseLaunch(factory, spawnOpts, opts = {}) {
|
|
84
|
+
const windowMs = opts.windowMs ?? exports.DEFAULT_LAUNCH_WINDOW_MS;
|
|
85
|
+
const now = opts.now ?? Date.now;
|
|
86
|
+
const log = opts.log ?? ((m) => console.error(m));
|
|
87
|
+
const settle = opts.settle ?? ((fn) => { setTimeout(fn, 100); });
|
|
88
|
+
const dataListeners = [];
|
|
89
|
+
const exitListeners = [];
|
|
90
|
+
const emitData = (d) => { for (const cb of dataListeners)
|
|
91
|
+
cb(d); };
|
|
92
|
+
const emitExit = (code) => { for (const cb of exitListeners)
|
|
93
|
+
cb(code); };
|
|
94
|
+
let cols = spawnOpts.cols;
|
|
95
|
+
let rows = spawnOpts.rows;
|
|
96
|
+
let args = spawnOpts.args ?? [];
|
|
97
|
+
let retried = false;
|
|
98
|
+
let killed = false;
|
|
99
|
+
let current;
|
|
100
|
+
const start = () => {
|
|
101
|
+
const startedAt = now();
|
|
102
|
+
let captured = '';
|
|
103
|
+
const pty = factory.spawn({ ...spawnOpts, cols, rows, args });
|
|
104
|
+
current = pty;
|
|
105
|
+
pty.onData((d) => {
|
|
106
|
+
if (pty !== current)
|
|
107
|
+
return;
|
|
108
|
+
if (captured.length < MAX_CAPTURE && now() - startedAt < windowMs)
|
|
109
|
+
captured += d;
|
|
110
|
+
emitData(d);
|
|
111
|
+
});
|
|
112
|
+
pty.onExit((code) => {
|
|
113
|
+
if (pty !== current)
|
|
114
|
+
return;
|
|
115
|
+
const early = now() - startedAt < windowMs;
|
|
116
|
+
if (killed || !early || code === 0) {
|
|
117
|
+
emitExit(code);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
settle(() => onEarlyFailure(code));
|
|
121
|
+
});
|
|
122
|
+
const onEarlyFailure = (code) => {
|
|
123
|
+
if (pty !== current)
|
|
124
|
+
return;
|
|
125
|
+
if (killed) {
|
|
126
|
+
emitExit(code);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const raw = stripAnsi(captured).trim();
|
|
130
|
+
log(`[bridge] claude exited at startup (code ${code}, args: ${args.join(' ')}): ${raw || '(no output)'}`);
|
|
131
|
+
const nextArgs = retried ? null : retryArgsFor(args, captured);
|
|
132
|
+
if (nextArgs) {
|
|
133
|
+
retried = true;
|
|
134
|
+
log(`[bridge] relaunching claude once with: ${nextArgs.join(' ')}`);
|
|
135
|
+
args = nextArgs;
|
|
136
|
+
emitData(RETRY_CLEAR);
|
|
137
|
+
try {
|
|
138
|
+
start();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
log(`[bridge] claude relaunch failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
emitData(friendlyStartFailure(captured, code));
|
|
146
|
+
emitExit(code);
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
start();
|
|
150
|
+
return {
|
|
151
|
+
write: (d) => current.write(d),
|
|
152
|
+
resize: (c, r) => { cols = c; rows = r; current.resize(c, r); },
|
|
153
|
+
kill: () => { killed = true; current.kill(); },
|
|
154
|
+
onData: (cb) => { dataListeners.push(cb); },
|
|
155
|
+
onExit: (cb) => { exitListeners.push(cb); },
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -94,6 +94,9 @@ function writeLocalContext(input, deps) {
|
|
|
94
94
|
pointers.push(work);
|
|
95
95
|
if (input.memoryAvailable)
|
|
96
96
|
pointers.push((0, memoryPointer_1.memorySection)(input.memoryBrief));
|
|
97
|
+
const browser = (0, workGraphPointer_1.browserSection)(input);
|
|
98
|
+
if (browser)
|
|
99
|
+
pointers.push(browser);
|
|
97
100
|
// wmr-07 — the whole local file is bridge-owned, so "preserve the pushed
|
|
98
101
|
// content" means keeping what is already on disk below the pointer fence.
|
|
99
102
|
const existingBody = existed ? deps.readFile(contextPath) : '';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TermSnapshot } from './termSnapshot';
|
|
2
|
+
import { LaunchSupervisorOptions } from './launchSupervisor';
|
|
2
3
|
export interface PtyLike {
|
|
3
4
|
write(data: string): void;
|
|
4
5
|
resize(cols: number, rows: number): void;
|
|
@@ -83,6 +84,10 @@ export interface SessionManagerOptions {
|
|
|
83
84
|
snapshotFactory?: (cols: number, rows: number, opts?: {
|
|
84
85
|
scrollback?: number;
|
|
85
86
|
}) => TermSnapshot;
|
|
87
|
+
/** Startup supervision for `claude` launches (launchSupervisor.ts): one
|
|
88
|
+
* self-healing retry for a session-id/resume mismatch, else a plain
|
|
89
|
+
* "Claude couldn't start" line. Injectable clock/settle for tests. */
|
|
90
|
+
launchSupervisor?: LaunchSupervisorOptions;
|
|
86
91
|
}
|
|
87
92
|
/**
|
|
88
93
|
* lws-07 (ADR-0033 port): validates a client-supplied size pair for the
|
|
@@ -106,6 +111,7 @@ export declare class SessionManager {
|
|
|
106
111
|
private scheduleTimeout;
|
|
107
112
|
private cancelTimeout;
|
|
108
113
|
private snapshotFactory;
|
|
114
|
+
private launchSupervisor;
|
|
109
115
|
/** twr-16: cumulative count of attach-pipeline wedges (any stage timeout,
|
|
110
116
|
* or a serialize-wedge whose recovery retry ALSO timed out) since this
|
|
111
117
|
* SessionManager was created. Surfaced via attachFailures() into the
|
|
@@ -21,6 +21,7 @@ exports.validSize = validSize;
|
|
|
21
21
|
*/
|
|
22
22
|
const crypto_1 = __importDefault(require("crypto"));
|
|
23
23
|
const termSnapshot_1 = require("./termSnapshot");
|
|
24
|
+
const launchSupervisor_1 = require("./launchSupervisor");
|
|
24
25
|
/** Default settle window (lws-07): after a resize-on-attach, how long we give
|
|
25
26
|
* the PTY's SIGWINCH-triggered repaint to land in the snapshot before
|
|
26
27
|
* serializing. Small — this only needs to cover one render tick, not a
|
|
@@ -73,6 +74,7 @@ class SessionManager {
|
|
|
73
74
|
scheduleTimeout;
|
|
74
75
|
cancelTimeout;
|
|
75
76
|
snapshotFactory;
|
|
77
|
+
launchSupervisor;
|
|
76
78
|
/** twr-16: cumulative count of attach-pipeline wedges (any stage timeout,
|
|
77
79
|
* or a serialize-wedge whose recovery retry ALSO timed out) since this
|
|
78
80
|
* SessionManager was created. Surfaced via attachFailures() into the
|
|
@@ -89,6 +91,7 @@ class SessionManager {
|
|
|
89
91
|
this.scheduleTimeout = opts.scheduleTimeout ?? ((cb, ms) => setTimeout(cb, ms));
|
|
90
92
|
this.cancelTimeout = opts.cancelTimeout ?? ((h) => clearTimeout(h));
|
|
91
93
|
this.snapshotFactory = opts.snapshotFactory ?? termSnapshot_1.createTermSnapshot;
|
|
94
|
+
this.launchSupervisor = opts.launchSupervisor ?? {};
|
|
92
95
|
}
|
|
93
96
|
/** twr-16: see attachFailureCount. */
|
|
94
97
|
attachFailures() {
|
|
@@ -123,7 +126,12 @@ class SessionManager {
|
|
|
123
126
|
prior.snapshot.dispose();
|
|
124
127
|
this.sessions.delete(id);
|
|
125
128
|
}
|
|
126
|
-
|
|
129
|
+
// A `claude` launch (agent panes, crew members) is supervised through its
|
|
130
|
+
// startup window so a fast failure heals or explains itself; a shell is
|
|
131
|
+
// spawned exactly as before.
|
|
132
|
+
const pty = opts.command === 'claude'
|
|
133
|
+
? (0, launchSupervisor_1.superviseLaunch)(this.factory, opts, this.launchSupervisor)
|
|
134
|
+
: this.factory.spawn(opts);
|
|
127
135
|
const snapshot = this.snapshotFactory(opts.cols, opts.rows, { scrollback: this.snapshotScrollback });
|
|
128
136
|
const session = {
|
|
129
137
|
pty,
|
|
@@ -51,3 +51,19 @@ export declare function workSection(input: {
|
|
|
51
51
|
workGraphAvailable?: boolean;
|
|
52
52
|
match?: boolean;
|
|
53
53
|
}): string | null;
|
|
54
|
+
/**
|
|
55
|
+
* cvt-09 (amended by the 2026-10-04 show-in-browser-tabs change) — tells the
|
|
56
|
+
* agent its pilot watches a Browser pane and WHEN to call `show_in_browser`:
|
|
57
|
+
* after starting a dev server or finishing a deploy, once the URL is confirmed
|
|
58
|
+
* live. Without this, agents only showed a site when asked.
|
|
59
|
+
*
|
|
60
|
+
* Gated on the same signal as the work-queue pointer (`workGraphAvailable` =
|
|
61
|
+
* the HiveOp MCP tools are registered for this process) — `show_in_browser`
|
|
62
|
+
* rides that same registration, and a pointer to an unregistered tool is
|
|
63
|
+
* worse than silence. Carried in matches too: a match agent runs dev servers.
|
|
64
|
+
*/
|
|
65
|
+
export declare const BROWSER_POINTER: string;
|
|
66
|
+
/** cvt-09 — the browser section, one function so the hosted and local writers cannot diverge. */
|
|
67
|
+
export declare function browserSection(input: {
|
|
68
|
+
workGraphAvailable?: boolean;
|
|
69
|
+
}): string | null;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MATCH_POINTER = exports.WORK_GRAPH_POINTER = void 0;
|
|
3
|
+
exports.BROWSER_POINTER = exports.MATCH_POINTER = exports.WORK_GRAPH_POINTER = void 0;
|
|
4
4
|
exports.workSection = workSection;
|
|
5
|
+
exports.browserSection = browserSection;
|
|
5
6
|
/**
|
|
6
7
|
* wgcp-01 — the one piece of guidance that tells an agent the workspace work
|
|
7
8
|
* graph exists at all. Nothing today does: the MCP tools (`list_ready_issues`,
|
|
@@ -64,3 +65,22 @@ function workSection(input) {
|
|
|
64
65
|
return exports.MATCH_POINTER;
|
|
65
66
|
return input.workGraphAvailable ? exports.WORK_GRAPH_POINTER : null;
|
|
66
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* cvt-09 (amended by the 2026-10-04 show-in-browser-tabs change) — tells the
|
|
70
|
+
* agent its pilot watches a Browser pane and WHEN to call `show_in_browser`:
|
|
71
|
+
* after starting a dev server or finishing a deploy, once the URL is confirmed
|
|
72
|
+
* live. Without this, agents only showed a site when asked.
|
|
73
|
+
*
|
|
74
|
+
* Gated on the same signal as the work-queue pointer (`workGraphAvailable` =
|
|
75
|
+
* the HiveOp MCP tools are registered for this process) — `show_in_browser`
|
|
76
|
+
* rides that same registration, and a pointer to an unregistered tool is
|
|
77
|
+
* worse than silence. Carried in matches too: a match agent runs dev servers.
|
|
78
|
+
*/
|
|
79
|
+
exports.BROWSER_POINTER = [
|
|
80
|
+
'## Browser',
|
|
81
|
+
'Your pilot watches a Browser pane on their HiveOp canvas. After you start a dev server or finish a deploy, confirm the URL is live (fetch it), then call `show_in_browser` with that URL. It reloads their tab if open, otherwise opens it in a background tab — it never touches their other tabs.',
|
|
82
|
+
].join('\n');
|
|
83
|
+
/** cvt-09 — the browser section, one function so the hosted and local writers cannot diverge. */
|
|
84
|
+
function browserSection(input) {
|
|
85
|
+
return input.workGraphAvailable ? exports.BROWSER_POINTER : null;
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wenrwa/marketplace-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "CLI tool for the Wenrwa Agent Marketplace \u2014 monitor bounties, agents, workspaces, and treasury from your terminal",
|
|
5
5
|
"author": "Wenrwa <dev@wenrwa.com>",
|
|
6
6
|
"license": "MIT",
|