@wenrwa/marketplace-cli 0.4.5 → 0.4.6
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-activity-parser.test.d.ts +1 -0
- package/dist/__tests__/bridge-activity-parser.test.js +0 -0
- package/dist/__tests__/bridge-activity-tap.test.d.ts +1 -0
- package/dist/__tests__/bridge-activity-tap.test.js +178 -0
- package/dist/__tests__/bridge-api-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-api-client.test.js +69 -0
- package/dist/__tests__/bridge-browser-driver.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-driver.test.js +379 -0
- package/dist/__tests__/bridge-browser-frames.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-frames.test.js +166 -0
- package/dist/__tests__/bridge-browser-idle.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-idle.test.js +259 -0
- package/dist/__tests__/bridge-browser-relay.test.d.ts +1 -0
- package/dist/__tests__/bridge-browser-relay.test.js +670 -0
- package/dist/__tests__/bridge-bundle-installer.test.d.ts +1 -0
- package/dist/__tests__/bridge-bundle-installer.test.js +126 -0
- package/dist/__tests__/bridge-cdp-client.test.d.ts +1 -0
- package/dist/__tests__/bridge-cdp-client.test.js +133 -0
- package/dist/__tests__/bridge-chromium-launcher.test.d.ts +1 -0
- package/dist/__tests__/bridge-chromium-launcher.test.js +202 -0
- package/dist/__tests__/bridge-command-browser-options.test.d.ts +1 -0
- package/dist/__tests__/bridge-command-browser-options.test.js +37 -0
- package/dist/__tests__/bridge-config-mcp.test.js +4 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-manifest-frame.test.js +125 -0
- package/dist/__tests__/bridge-context-writer.test.d.ts +1 -0
- package/dist/__tests__/bridge-context-writer.test.js +161 -0
- package/dist/__tests__/bridge-device-registration.test.d.ts +1 -0
- package/dist/__tests__/bridge-device-registration.test.js +33 -0
- package/dist/__tests__/bridge-folder-picker-runner.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-picker-runner.test.js +37 -0
- package/dist/__tests__/bridge-folder-picker.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-picker.test.js +66 -0
- package/dist/__tests__/bridge-folder-registry.test.d.ts +1 -0
- package/dist/__tests__/bridge-folder-registry.test.js +107 -0
- package/dist/__tests__/bridge-frame-pacer.test.d.ts +1 -0
- package/dist/__tests__/bridge-frame-pacer.test.js +102 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.d.ts +1 -0
- package/dist/__tests__/bridge-install-bundle-frame.test.js +107 -0
- package/dist/__tests__/bridge-local-add-core.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-add-core.test.js +48 -0
- package/dist/__tests__/bridge-local-path-validation-fs.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-path-validation-fs.test.js +88 -0
- package/dist/__tests__/bridge-local-path-validation.test.d.ts +1 -0
- package/dist/__tests__/bridge-local-path-validation.test.js +45 -0
- package/dist/__tests__/bridge-login-status.test.d.ts +1 -0
- package/dist/__tests__/bridge-login-status.test.js +60 -0
- package/dist/__tests__/bridge-relay-pucc14.test.d.ts +1 -0
- package/dist/__tests__/bridge-relay-pucc14.test.js +230 -0
- package/dist/__tests__/bridge-server-local-endpoints.test.d.ts +1 -0
- package/dist/__tests__/bridge-server-local-endpoints.test.js +197 -0
- package/dist/__tests__/bridge-server.test.js +22 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.d.ts +1 -0
- package/dist/__tests__/bridge-workspace-multirepo.test.js +317 -0
- package/dist/__tests__/devices.test.d.ts +1 -0
- package/dist/__tests__/devices.test.js +66 -0
- package/dist/__tests__/local.test.d.ts +1 -0
- package/dist/__tests__/local.test.js +147 -0
- package/dist/bridge/activityParser.d.ts +53 -0
- package/dist/bridge/activityParser.js +202 -0
- package/dist/bridge/apiClient.d.ts +56 -0
- package/dist/bridge/apiClient.js +50 -0
- package/dist/bridge/browserDriver.d.ts +98 -0
- package/dist/bridge/browserDriver.js +265 -0
- package/dist/bridge/browserFrames.d.ts +182 -0
- package/dist/bridge/browserFrames.js +179 -0
- package/dist/bridge/browserIdle.d.ts +75 -0
- package/dist/bridge/browserIdle.js +120 -0
- package/dist/bridge/bundleInstaller.d.ts +31 -0
- package/dist/bridge/bundleInstaller.js +127 -0
- package/dist/bridge/cdpClient.d.ts +47 -0
- package/dist/bridge/cdpClient.js +102 -0
- package/dist/bridge/chromiumLauncher.d.ts +45 -0
- package/dist/bridge/chromiumLauncher.js +113 -0
- package/dist/bridge/config.d.ts +3 -0
- package/dist/bridge/contextWriter.d.ts +31 -0
- package/dist/bridge/contextWriter.js +139 -0
- package/dist/bridge/deviceRegistration.d.ts +28 -0
- package/dist/bridge/deviceRegistration.js +19 -0
- package/dist/bridge/folderPicker.d.ts +34 -0
- package/dist/bridge/folderPicker.js +36 -0
- package/dist/bridge/folderPickerRunner.d.ts +3 -0
- package/dist/bridge/folderPickerRunner.js +25 -0
- package/dist/bridge/folderRegistry.d.ts +19 -0
- package/dist/bridge/folderRegistry.js +100 -0
- package/dist/bridge/framePacer.d.ts +61 -0
- package/dist/bridge/framePacer.js +79 -0
- package/dist/bridge/localAdd.d.ts +28 -0
- package/dist/bridge/localAdd.js +67 -0
- package/dist/bridge/localPathValidation.d.ts +20 -0
- package/dist/bridge/localPathValidation.js +58 -0
- package/dist/bridge/localPathValidationFs.d.ts +2 -0
- package/dist/bridge/localPathValidationFs.js +98 -0
- package/dist/bridge/loginStatus.d.ts +38 -0
- package/dist/bridge/loginStatus.js +56 -0
- package/dist/bridge/realPty.js +6 -2
- package/dist/bridge/relayClient.d.ts +124 -0
- package/dist/bridge/relayClient.js +368 -5
- package/dist/bridge/repoCheckout.d.ts +31 -0
- package/dist/bridge/repoCheckout.js +18 -0
- package/dist/bridge/server.d.ts +33 -0
- package/dist/bridge/server.js +98 -10
- package/dist/bridge/sessionManager.d.ts +18 -0
- package/dist/bridge/sessionManager.js +15 -1
- package/dist/commands/bridge.d.ts +6 -0
- package/dist/commands/bridge.js +155 -1
- package/dist/commands/devices.d.ts +9 -0
- package/dist/commands/devices.js +53 -0
- package/dist/commands/local.d.ts +9 -0
- package/dist/commands/local.js +141 -0
- package/dist/index.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
/**
|
|
7
|
+
* TDD — lws-03: `wen-market local add/list/remove`. Folder→workspace
|
|
8
|
+
* registry commands. Mocks the bridge config/apiClient/folderRegistry
|
|
9
|
+
* modules — no real network, no real fs (prior art: workspace.test.ts).
|
|
10
|
+
*/
|
|
11
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
12
|
+
const mockLoadBridgeConfig = jest.fn();
|
|
13
|
+
jest.mock('../bridge/config', () => ({
|
|
14
|
+
loadBridgeConfig: () => mockLoadBridgeConfig(),
|
|
15
|
+
}));
|
|
16
|
+
const mockCreateLocalWorkspace = jest.fn();
|
|
17
|
+
const mockDeleteWorkspace = jest.fn();
|
|
18
|
+
jest.mock('../bridge/apiClient', () => ({
|
|
19
|
+
createLocalWorkspace: (...args) => mockCreateLocalWorkspace(...args),
|
|
20
|
+
deleteWorkspace: (...args) => mockDeleteWorkspace(...args),
|
|
21
|
+
BridgeApiError: class BridgeApiError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
status;
|
|
24
|
+
constructor(message, code, status) { super(message); this.code = code; this.status = status; }
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
jest.mock('../commands/bridge', () => ({ bridgeApiUrl: () => 'https://api.x' }));
|
|
28
|
+
let registryState = { v: 1, entries: {} };
|
|
29
|
+
jest.mock('../bridge/folderRegistry', () => {
|
|
30
|
+
const actual = jest.requireActual('../bridge/folderRegistry');
|
|
31
|
+
return {
|
|
32
|
+
...actual,
|
|
33
|
+
loadFolderRegistry: () => registryState,
|
|
34
|
+
saveFolderRegistry: (registry) => { registryState = registry; },
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
const local_1 = require("../commands/local");
|
|
38
|
+
async function runCommand(...args) {
|
|
39
|
+
await local_1.localCommand.parseAsync(args, { from: 'user' });
|
|
40
|
+
}
|
|
41
|
+
beforeAll(() => {
|
|
42
|
+
chalk_1.default.level = 0;
|
|
43
|
+
});
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
jest.clearAllMocks();
|
|
46
|
+
registryState = { v: 1, entries: {} };
|
|
47
|
+
mockLoadBridgeConfig.mockReturnValue({ connectToken: 'wm_ct_' + 'a'.repeat(64), deviceId: 'device-1' });
|
|
48
|
+
});
|
|
49
|
+
describe('local add', () => {
|
|
50
|
+
it('creates a local workspace and registers the resolved path', async () => {
|
|
51
|
+
mockCreateLocalWorkspace.mockResolvedValue({ success: true, workspace: { id: 'ws-1', name: 'wenrwa-arena' } });
|
|
52
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
53
|
+
await runCommand('add', process.cwd());
|
|
54
|
+
expect(mockCreateLocalWorkspace).toHaveBeenCalledWith(expect.objectContaining({ apiUrl: 'https://api.x', connectToken: expect.stringMatching(/^wm_ct_/) }), { name: expect.any(String) });
|
|
55
|
+
expect(registryState.entries[process.cwd()]).toMatchObject({ workspaceId: 'ws-1' });
|
|
56
|
+
const output = spy.mock.calls.map((c) => c[0]).join('\n');
|
|
57
|
+
expect(output).toContain('ws-1');
|
|
58
|
+
spy.mockRestore();
|
|
59
|
+
});
|
|
60
|
+
it('is idempotent — re-adding the same path returns the existing workspace without creating a new one', async () => {
|
|
61
|
+
registryState = { v: 1, entries: { [process.cwd()]: { workspaceId: 'ws-existing', name: 'x', addedAt: 'now' } } };
|
|
62
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
63
|
+
await runCommand('add', process.cwd());
|
|
64
|
+
expect(mockCreateLocalWorkspace).not.toHaveBeenCalled();
|
|
65
|
+
const output = spy.mock.calls.map((c) => c[0]).join('\n');
|
|
66
|
+
expect(output).toContain('ws-existing');
|
|
67
|
+
spy.mockRestore();
|
|
68
|
+
});
|
|
69
|
+
it('errors clearly when this machine has not been connected yet (no deviceId)', async () => {
|
|
70
|
+
mockLoadBridgeConfig.mockReturnValue({ connectToken: 'wm_ct_' + 'a'.repeat(64) }); // no deviceId
|
|
71
|
+
const errSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
72
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
73
|
+
await runCommand('add', process.cwd());
|
|
74
|
+
expect(mockCreateLocalWorkspace).not.toHaveBeenCalled();
|
|
75
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
76
|
+
errSpy.mockRestore();
|
|
77
|
+
exitSpy.mockRestore();
|
|
78
|
+
});
|
|
79
|
+
it('surfaces a clear, non-crashing error when the device was revoked (401 mid-registration)', async () => {
|
|
80
|
+
const { BridgeApiError } = jest.requireMock('../bridge/apiClient');
|
|
81
|
+
mockCreateLocalWorkspace.mockRejectedValue(new BridgeApiError('Connect token is invalid or revoked.', 'INVALID_CONNECT_TOKEN', 401));
|
|
82
|
+
const errSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
83
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
84
|
+
await runCommand('add', process.cwd());
|
|
85
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
86
|
+
const output = errSpy.mock.calls.map((c) => c[0]).join('\n');
|
|
87
|
+
expect(output.toLowerCase()).toContain('revoked');
|
|
88
|
+
errSpy.mockRestore();
|
|
89
|
+
exitSpy.mockRestore();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('local list', () => {
|
|
93
|
+
it('prints all registered folders', async () => {
|
|
94
|
+
registryState = {
|
|
95
|
+
v: 1,
|
|
96
|
+
entries: {
|
|
97
|
+
'/Users/x/app-a': { workspaceId: 'ws-a', name: 'app-a', addedAt: 'now' },
|
|
98
|
+
'/Users/x/app-b': { workspaceId: 'ws-b', name: 'app-b', addedAt: 'now' },
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
102
|
+
await runCommand('list');
|
|
103
|
+
const output = spy.mock.calls.map((c) => c[0]).join('\n');
|
|
104
|
+
expect(output).toContain('ws-a');
|
|
105
|
+
expect(output).toContain('ws-b');
|
|
106
|
+
spy.mockRestore();
|
|
107
|
+
});
|
|
108
|
+
it('says so when nothing is registered', async () => {
|
|
109
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
110
|
+
await runCommand('list');
|
|
111
|
+
expect(spy).toHaveBeenCalledWith(expect.stringContaining('No local workspaces registered'));
|
|
112
|
+
spy.mockRestore();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('local remove', () => {
|
|
116
|
+
it('removes the registration only — never deletes the workspace by default', async () => {
|
|
117
|
+
registryState = { v: 1, entries: { '/Users/x/app-a': { workspaceId: 'ws-a', name: 'app-a', addedAt: 'now' } } };
|
|
118
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
119
|
+
await runCommand('remove', '/Users/x/app-a');
|
|
120
|
+
expect(registryState.entries['/Users/x/app-a']).toBeUndefined();
|
|
121
|
+
expect(mockDeleteWorkspace).not.toHaveBeenCalled();
|
|
122
|
+
const output = spy.mock.calls.map((c) => c[0]).join('\n');
|
|
123
|
+
expect(output.toLowerCase()).toContain('files untouched');
|
|
124
|
+
spy.mockRestore();
|
|
125
|
+
});
|
|
126
|
+
it('finds the entry by workspaceId too, not just by path', async () => {
|
|
127
|
+
registryState = { v: 1, entries: { '/Users/x/app-a': { workspaceId: 'ws-a', name: 'app-a', addedAt: 'now' } } };
|
|
128
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
129
|
+
await runCommand('remove', 'ws-a');
|
|
130
|
+
expect(registryState.entries['/Users/x/app-a']).toBeUndefined();
|
|
131
|
+
spy.mockRestore();
|
|
132
|
+
});
|
|
133
|
+
it('--delete also deletes the workspace row server-side', async () => {
|
|
134
|
+
registryState = { v: 1, entries: { '/Users/x/app-a': { workspaceId: 'ws-a', name: 'app-a', addedAt: 'now' } } };
|
|
135
|
+
mockDeleteWorkspace.mockResolvedValue({ success: true });
|
|
136
|
+
const spy = jest.spyOn(console, 'log').mockImplementation();
|
|
137
|
+
await runCommand('remove', '/Users/x/app-a', '--delete');
|
|
138
|
+
expect(mockDeleteWorkspace).toHaveBeenCalledWith(expect.anything(), 'ws-a');
|
|
139
|
+
spy.mockRestore();
|
|
140
|
+
});
|
|
141
|
+
it('errors clearly when the path/id is not registered', async () => {
|
|
142
|
+
const errSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
143
|
+
await runCommand('remove', '/nope');
|
|
144
|
+
expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('not registered'));
|
|
145
|
+
errSpy.mockRestore();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pucc-int-03 (ADR-0032) — Claude Code PTY activity parser (the PRODUCER).
|
|
3
|
+
*
|
|
4
|
+
* A Claude Code agent is a real PTY session the platform never drives
|
|
5
|
+
* (ADR-0028). To give it the same file-targeted drones/holograms a credits
|
|
6
|
+
* agent gets, the bridge OBSERVES its PTY output (read-only) and extracts the
|
|
7
|
+
* tool-use it renders — Edit/Write/Read/Bash targets + a short "thinking"
|
|
8
|
+
* summary — so the arena can swarm the files it touches.
|
|
9
|
+
*
|
|
10
|
+
* This module is PURE and defensive by contract:
|
|
11
|
+
* - Input is raw PTY output: ANSI escapes, \r cursor redraws, and tool lines
|
|
12
|
+
* split arbitrarily across chunks. It buffers partial lines across `push`.
|
|
13
|
+
* - A parse MISS yields NOTHING (empty array). It NEVER throws — every code
|
|
14
|
+
* path that could fail is caught and degraded to "no signal", because a
|
|
15
|
+
* parser bug must never break the terminal it is only watching.
|
|
16
|
+
* - The recognized markers are a SMALL, VERSIONED set (see TOOL_VERBS): if
|
|
17
|
+
* Claude Code's output format drifts, this table is the one thing to bump.
|
|
18
|
+
*
|
|
19
|
+
* Recognized shape (best-effort): Claude Code renders a tool call as a bulleted
|
|
20
|
+
* line — `● Edit(src/foo.ts)`, `⏺ Read(package.json)`, `● Bash(npm test)`,
|
|
21
|
+
* `⏺ Update(src/x.ts)` — a bullet glyph, the Tool name, and a parenthesized
|
|
22
|
+
* arg. For file tools the arg's first token is the (repo-relative) path; for
|
|
23
|
+
* Bash it is a command (no path is extracted — the consumer degrades an empty
|
|
24
|
+
* `filePaths` to an ambient pulse). Every recognized tool also yields a
|
|
25
|
+
* human "thinking" phrase ("Editing foo.ts", "Running npm test").
|
|
26
|
+
*/
|
|
27
|
+
export interface ActivitySignal {
|
|
28
|
+
/** Repo-relative file targets extracted from a file-tool call (may be empty). */
|
|
29
|
+
filePaths: string[];
|
|
30
|
+
/** Short human summary of what the agent is doing, e.g. "Editing foo.ts". */
|
|
31
|
+
thinking?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface ActivityParserOpts {
|
|
34
|
+
/** A single un-terminated line longer than this is abandoned (binary spew /
|
|
35
|
+
* a pathological redraw) — the buffer is trimmed to its tail so a later
|
|
36
|
+
* newline can still complete a plausible line. Default 16384. */
|
|
37
|
+
maxLineLength?: number;
|
|
38
|
+
}
|
|
39
|
+
export declare class ActivityParser {
|
|
40
|
+
private buf;
|
|
41
|
+
private maxLineLength;
|
|
42
|
+
/** Suppress consecutive identical signals (TUI re-renders the same line). */
|
|
43
|
+
private lastKey;
|
|
44
|
+
constructor(opts?: ActivityParserOpts);
|
|
45
|
+
/**
|
|
46
|
+
* Feed a chunk of PTY output. Returns every NEW tool-use signal completed by
|
|
47
|
+
* this chunk (usually 0 or 1). Never throws: any internal failure degrades
|
|
48
|
+
* to an empty result so the caller's PTY forwarding is never disturbed.
|
|
49
|
+
*/
|
|
50
|
+
push(chunk: string): ActivitySignal[];
|
|
51
|
+
private pushUnsafe;
|
|
52
|
+
private parseLine;
|
|
53
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* pucc-int-03 (ADR-0032) — Claude Code PTY activity parser (the PRODUCER).
|
|
4
|
+
*
|
|
5
|
+
* A Claude Code agent is a real PTY session the platform never drives
|
|
6
|
+
* (ADR-0028). To give it the same file-targeted drones/holograms a credits
|
|
7
|
+
* agent gets, the bridge OBSERVES its PTY output (read-only) and extracts the
|
|
8
|
+
* tool-use it renders — Edit/Write/Read/Bash targets + a short "thinking"
|
|
9
|
+
* summary — so the arena can swarm the files it touches.
|
|
10
|
+
*
|
|
11
|
+
* This module is PURE and defensive by contract:
|
|
12
|
+
* - Input is raw PTY output: ANSI escapes, \r cursor redraws, and tool lines
|
|
13
|
+
* split arbitrarily across chunks. It buffers partial lines across `push`.
|
|
14
|
+
* - A parse MISS yields NOTHING (empty array). It NEVER throws — every code
|
|
15
|
+
* path that could fail is caught and degraded to "no signal", because a
|
|
16
|
+
* parser bug must never break the terminal it is only watching.
|
|
17
|
+
* - The recognized markers are a SMALL, VERSIONED set (see TOOL_VERBS): if
|
|
18
|
+
* Claude Code's output format drifts, this table is the one thing to bump.
|
|
19
|
+
*
|
|
20
|
+
* Recognized shape (best-effort): Claude Code renders a tool call as a bulleted
|
|
21
|
+
* line — `● Edit(src/foo.ts)`, `⏺ Read(package.json)`, `● Bash(npm test)`,
|
|
22
|
+
* `⏺ Update(src/x.ts)` — a bullet glyph, the Tool name, and a parenthesized
|
|
23
|
+
* arg. For file tools the arg's first token is the (repo-relative) path; for
|
|
24
|
+
* Bash it is a command (no path is extracted — the consumer degrades an empty
|
|
25
|
+
* `filePaths` to an ambient pulse). Every recognized tool also yields a
|
|
26
|
+
* human "thinking" phrase ("Editing foo.ts", "Running npm test").
|
|
27
|
+
*/
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
exports.ActivityParser = void 0;
|
|
30
|
+
/**
|
|
31
|
+
* The VERSIONED marker table. Tool → gerund used in the "thinking" phrase.
|
|
32
|
+
* FILE_TOOLS is the subset whose arg is a path we extract into `filePaths`.
|
|
33
|
+
* Bash is recognized (for a thinking cue) but yields no path.
|
|
34
|
+
*/
|
|
35
|
+
const TOOL_VERBS = {
|
|
36
|
+
Read: 'Reading',
|
|
37
|
+
Edit: 'Editing',
|
|
38
|
+
Write: 'Writing',
|
|
39
|
+
Update: 'Updating',
|
|
40
|
+
Create: 'Creating',
|
|
41
|
+
MultiEdit: 'Editing',
|
|
42
|
+
NotebookEdit: 'Editing',
|
|
43
|
+
Bash: 'Running',
|
|
44
|
+
};
|
|
45
|
+
const FILE_TOOLS = new Set(['Read', 'Edit', 'Write', 'Update', 'Create', 'MultiEdit', 'NotebookEdit']);
|
|
46
|
+
/** Bullet glyphs Claude Code (and similar TUIs) prefix a tool line with. */
|
|
47
|
+
const BULLET = '[\\u25cf\\u23fa\\u23f5\\u25b6\\u25b7\\u2022*>]'; // ● ⏺ ⏵ ▶ ▷ • * >
|
|
48
|
+
/**
|
|
49
|
+
* `<bullet> <Tool>(<arg>` — arg captured up to the first ')' or end-of-line
|
|
50
|
+
* (Claude Code truncates long args with `…` and NO closing paren, so a missing
|
|
51
|
+
* ')' must still parse). Operates on a single already-newline-split, ANSI-free
|
|
52
|
+
* line, so `.` never crosses lines.
|
|
53
|
+
*/
|
|
54
|
+
const TOOL_LINE = new RegExp('^' + BULLET + '\\s+([A-Za-z][A-Za-z0-9_]*)\\(([^)]*)\\)?');
|
|
55
|
+
/**
|
|
56
|
+
* ANSI/VT control-sequence matchers, built from \\u001b (ESC) / \\u0007 (BEL)
|
|
57
|
+
* escapes so this source carries NO literal control bytes (which an editor can
|
|
58
|
+
* silently corrupt). Stateless with /g; String.replace resets lastIndex.
|
|
59
|
+
*/
|
|
60
|
+
const _ESC = "\\u001b";
|
|
61
|
+
const _BEL = "\\u0007";
|
|
62
|
+
// OSC: ESC ] ... (BEL | ESC \\)
|
|
63
|
+
const OSC = new RegExp(_ESC + '\\][\\s\\S]*?(?:' + _BEL + '|' + _ESC + '\\\\)', 'g');
|
|
64
|
+
// CSI + other ESC-introduced sequences (SGR colors, cursor moves, ...): ESC,
|
|
65
|
+
// optional intro chars, optional numeric params, a final byte in @-~.
|
|
66
|
+
const CSI = new RegExp(_ESC + '[[\\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[@-~]', 'g');
|
|
67
|
+
// Any remaining lone ESC + C0 control chars EXCEPT tab(09) newline(0a) cr(0d).
|
|
68
|
+
const STRAY = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
|
|
69
|
+
/** Strip ANSI/VT control sequences to leave printable text. */
|
|
70
|
+
function stripAnsi(input) {
|
|
71
|
+
return input.replace(OSC, "").replace(CSI, "").replace(STRAY, "");
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Reduce a raw line to its final RENDERED content. A TUI redraws in place with
|
|
75
|
+
* carriage returns: "old\rnew" displays "new". Keep only the text after the
|
|
76
|
+
* last \r so repeated redraws collapse to one signal.
|
|
77
|
+
*/
|
|
78
|
+
function finalRendered(line) {
|
|
79
|
+
const cr = line.lastIndexOf('\r');
|
|
80
|
+
return cr >= 0 ? line.slice(cr + 1) : line;
|
|
81
|
+
}
|
|
82
|
+
/** Extract a plausible repo-relative path from a file-tool arg, or null. */
|
|
83
|
+
function extractPath(arg) {
|
|
84
|
+
// Take the first comma-separated segment (Claude Code appends `, offset:…`).
|
|
85
|
+
let s = arg.split(',')[0].trim();
|
|
86
|
+
// Drop an optional `file_path:` / `path:` label if present.
|
|
87
|
+
const labelled = s.match(/^(?:file_path|path|notebook_path)\s*:\s*(.+)$/i);
|
|
88
|
+
if (labelled)
|
|
89
|
+
s = labelled[1].trim();
|
|
90
|
+
// Strip a single pair of surrounding quotes.
|
|
91
|
+
s = s.replace(/^["'`](.*)["'`]$/, '$1').trim();
|
|
92
|
+
// Strip a trailing truncation ellipsis Claude Code adds to long args.
|
|
93
|
+
s = s.replace(/(?:…|\.\.\.)+$/, '').trim();
|
|
94
|
+
// Strip a leading "./".
|
|
95
|
+
s = s.replace(/^\.\//, '');
|
|
96
|
+
if (!s || s.length > 512)
|
|
97
|
+
return null;
|
|
98
|
+
// Reject obvious non-paths (nothing path-like left after cleanup).
|
|
99
|
+
if (!/[A-Za-z0-9._/\\-]/.test(s))
|
|
100
|
+
return null;
|
|
101
|
+
return s;
|
|
102
|
+
}
|
|
103
|
+
function basename(p) {
|
|
104
|
+
const parts = p.split(/[\\/]/);
|
|
105
|
+
return parts[parts.length - 1] || p;
|
|
106
|
+
}
|
|
107
|
+
/** Truncate a thinking phrase to a bounded, single-line length. */
|
|
108
|
+
function boundThinking(s) {
|
|
109
|
+
const oneLine = s.replace(/\s+/g, ' ').trim();
|
|
110
|
+
return oneLine.length > 80 ? oneLine.slice(0, 79) + '…' : oneLine;
|
|
111
|
+
}
|
|
112
|
+
/** Best-effort short summary of a Bash arg for the thinking cue. */
|
|
113
|
+
function extractCommandSummary(arg) {
|
|
114
|
+
const s = arg
|
|
115
|
+
.replace(/(?:…|\.\.\.)+$/, '')
|
|
116
|
+
.trim()
|
|
117
|
+
.replace(/^["'`](.*)["'`]$/, '$1')
|
|
118
|
+
.trim();
|
|
119
|
+
return s || null;
|
|
120
|
+
}
|
|
121
|
+
const DEFAULT_MAX_LINE = 16384;
|
|
122
|
+
class ActivityParser {
|
|
123
|
+
buf = '';
|
|
124
|
+
maxLineLength;
|
|
125
|
+
/** Suppress consecutive identical signals (TUI re-renders the same line). */
|
|
126
|
+
lastKey = '';
|
|
127
|
+
constructor(opts = {}) {
|
|
128
|
+
this.maxLineLength = opts.maxLineLength ?? DEFAULT_MAX_LINE;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Feed a chunk of PTY output. Returns every NEW tool-use signal completed by
|
|
132
|
+
* this chunk (usually 0 or 1). Never throws: any internal failure degrades
|
|
133
|
+
* to an empty result so the caller's PTY forwarding is never disturbed.
|
|
134
|
+
*/
|
|
135
|
+
push(chunk) {
|
|
136
|
+
try {
|
|
137
|
+
return this.pushUnsafe(chunk);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// A parser bug must never break the terminal it only observes. Reset the
|
|
141
|
+
// buffer so one bad state can't wedge all future parsing.
|
|
142
|
+
this.buf = '';
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
pushUnsafe(chunk) {
|
|
147
|
+
if (typeof chunk !== 'string' || chunk.length === 0)
|
|
148
|
+
return [];
|
|
149
|
+
this.buf += chunk;
|
|
150
|
+
// Normalize CRLF line endings to LF so a terminal's trailing `\r\n` is a
|
|
151
|
+
// line break, not mistaken for a mid-line redraw. A lone trailing `\r`
|
|
152
|
+
// (chunk boundary) stays buffered — the next chunk's `\n` completes it.
|
|
153
|
+
this.buf = this.buf.replace(/\r\n/g, '\n');
|
|
154
|
+
// Bounded buffer: if a single line runs away without a newline (binary
|
|
155
|
+
// spew), keep only the tail so a future newline can still complete a line.
|
|
156
|
+
if (this.buf.length > this.maxLineLength && !this.buf.includes('\n')) {
|
|
157
|
+
this.buf = this.buf.slice(-this.maxLineLength);
|
|
158
|
+
}
|
|
159
|
+
const nl = this.buf.lastIndexOf('\n');
|
|
160
|
+
if (nl < 0)
|
|
161
|
+
return []; // no complete line yet — keep buffering
|
|
162
|
+
const complete = this.buf.slice(0, nl);
|
|
163
|
+
this.buf = this.buf.slice(nl + 1);
|
|
164
|
+
const out = [];
|
|
165
|
+
for (const rawLine of complete.split('\n')) {
|
|
166
|
+
const signal = this.parseLine(rawLine);
|
|
167
|
+
if (!signal)
|
|
168
|
+
continue;
|
|
169
|
+
const key = JSON.stringify(signal);
|
|
170
|
+
if (key === this.lastKey)
|
|
171
|
+
continue; // dedup rapid repeats
|
|
172
|
+
this.lastKey = key;
|
|
173
|
+
out.push(signal);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
parseLine(rawLine) {
|
|
178
|
+
const line = stripAnsi(finalRendered(rawLine)).trim();
|
|
179
|
+
if (!line)
|
|
180
|
+
return null;
|
|
181
|
+
const m = TOOL_LINE.exec(line);
|
|
182
|
+
if (!m)
|
|
183
|
+
return null;
|
|
184
|
+
const tool = m[1];
|
|
185
|
+
const verb = TOOL_VERBS[tool];
|
|
186
|
+
if (!verb)
|
|
187
|
+
return null; // unknown tool → no signal (fewer false events)
|
|
188
|
+
const arg = m[2] ?? '';
|
|
189
|
+
if (FILE_TOOLS.has(tool)) {
|
|
190
|
+
const path = extractPath(arg);
|
|
191
|
+
if (path) {
|
|
192
|
+
return { filePaths: [path], thinking: boundThinking(verb + ' ' + basename(path)) };
|
|
193
|
+
}
|
|
194
|
+
// Recognized file tool but no usable path → thinking-only (ambient pulse).
|
|
195
|
+
return { filePaths: [], thinking: boundThinking(verb) };
|
|
196
|
+
}
|
|
197
|
+
// Bash (and any future non-file tool): a thinking cue, no path.
|
|
198
|
+
const cmd = extractCommandSummary(arg);
|
|
199
|
+
return { filePaths: [], thinking: boundThinking(cmd ? verb + ' ' + cmd : verb) };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
exports.ActivityParser = ActivityParser;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lws-03 — bridge REST client. The daemon talks to the backend AS ITSELF
|
|
3
|
+
* (Authorization: Bearer <Connect Token>), distinct from utils/client.ts
|
|
4
|
+
* (the interactive CLI's X-Wallet-Pubkey login flow). Used for device
|
|
5
|
+
* pairing ("Connect this machine") and local-workspace creation/deletion.
|
|
6
|
+
* fetchImpl is injectable so callers/tests never hit the network.
|
|
7
|
+
*/
|
|
8
|
+
export type FetchImpl = (url: string, init: {
|
|
9
|
+
method: string;
|
|
10
|
+
headers: Record<string, string>;
|
|
11
|
+
body?: string;
|
|
12
|
+
}) => Promise<{
|
|
13
|
+
ok: boolean;
|
|
14
|
+
status: number;
|
|
15
|
+
json: () => Promise<any>;
|
|
16
|
+
}>;
|
|
17
|
+
export interface BridgeApiOpts {
|
|
18
|
+
apiUrl: string;
|
|
19
|
+
connectToken: string;
|
|
20
|
+
fetchImpl?: FetchImpl;
|
|
21
|
+
}
|
|
22
|
+
export declare class BridgeApiError extends Error {
|
|
23
|
+
code: string;
|
|
24
|
+
status: number;
|
|
25
|
+
constructor(message: string, code: string, status: number);
|
|
26
|
+
}
|
|
27
|
+
export interface DeviceRecord {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
createdAt?: string;
|
|
31
|
+
revokedAt?: string | null;
|
|
32
|
+
}
|
|
33
|
+
export declare function registerDevice(opts: BridgeApiOpts, name: string): Promise<{
|
|
34
|
+
success: boolean;
|
|
35
|
+
device: DeviceRecord;
|
|
36
|
+
}>;
|
|
37
|
+
export declare function listDevices(opts: BridgeApiOpts): Promise<{
|
|
38
|
+
success: boolean;
|
|
39
|
+
devices: DeviceRecord[];
|
|
40
|
+
}>;
|
|
41
|
+
export declare function revokeDevice(opts: BridgeApiOpts, deviceId: string): Promise<{
|
|
42
|
+
success: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
export interface WorkspaceRecord {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
}
|
|
48
|
+
export declare function createLocalWorkspace(opts: BridgeApiOpts, params: {
|
|
49
|
+
name: string;
|
|
50
|
+
}): Promise<{
|
|
51
|
+
success: boolean;
|
|
52
|
+
workspace: WorkspaceRecord;
|
|
53
|
+
}>;
|
|
54
|
+
export declare function deleteWorkspace(opts: BridgeApiOpts, workspaceId: string): Promise<{
|
|
55
|
+
success: boolean;
|
|
56
|
+
}>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BridgeApiError = void 0;
|
|
4
|
+
exports.registerDevice = registerDevice;
|
|
5
|
+
exports.listDevices = listDevices;
|
|
6
|
+
exports.revokeDevice = revokeDevice;
|
|
7
|
+
exports.createLocalWorkspace = createLocalWorkspace;
|
|
8
|
+
exports.deleteWorkspace = deleteWorkspace;
|
|
9
|
+
class BridgeApiError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
status;
|
|
12
|
+
constructor(message, code, status) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.name = 'BridgeApiError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.BridgeApiError = BridgeApiError;
|
|
20
|
+
async function bridgeRequest(opts, method, path, body) {
|
|
21
|
+
const f = opts.fetchImpl ?? fetch;
|
|
22
|
+
const res = await f(`${opts.apiUrl}${path}`, {
|
|
23
|
+
method,
|
|
24
|
+
headers: {
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
Authorization: `Bearer ${opts.connectToken}`,
|
|
27
|
+
},
|
|
28
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
29
|
+
});
|
|
30
|
+
const json = await res.json();
|
|
31
|
+
if (!res.ok || json?.success === false) {
|
|
32
|
+
throw new BridgeApiError(json?.error?.message || `HTTP ${res.status}`, json?.error?.code || 'UNKNOWN', res.status);
|
|
33
|
+
}
|
|
34
|
+
return json;
|
|
35
|
+
}
|
|
36
|
+
function registerDevice(opts, name) {
|
|
37
|
+
return bridgeRequest(opts, 'POST', '/me/devices', { name });
|
|
38
|
+
}
|
|
39
|
+
function listDevices(opts) {
|
|
40
|
+
return bridgeRequest(opts, 'GET', '/me/devices');
|
|
41
|
+
}
|
|
42
|
+
function revokeDevice(opts, deviceId) {
|
|
43
|
+
return bridgeRequest(opts, 'DELETE', `/me/devices/${deviceId}`);
|
|
44
|
+
}
|
|
45
|
+
function createLocalWorkspace(opts, params) {
|
|
46
|
+
return bridgeRequest(opts, 'POST', '/workspaces', { name: params.name, kind: 'local' });
|
|
47
|
+
}
|
|
48
|
+
function deleteWorkspace(opts, workspaceId) {
|
|
49
|
+
return bridgeRequest(opts, 'DELETE', `/workspaces/${workspaceId}`);
|
|
50
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cbs-02 — bridge CDP browser driver.
|
|
3
|
+
*
|
|
4
|
+
* (docs/prd/cloud-browser-session.md, issues/cbs-02-bridge-cdp-driver.md)
|
|
5
|
+
* The deep module inside the in-container bridge that owns one headless
|
|
6
|
+
* Chromium tab: spawn (child_process, NOT node-pty/SessionManager — this
|
|
7
|
+
* must never touch the PTY session manager or spawn a shell), a minimal
|
|
8
|
+
* hand-rolled CDP client (cdpClient.ts) driving `Page.startScreencast` for
|
|
9
|
+
* output and `Input.dispatch*`/`insertText` for input, screencast frames
|
|
10
|
+
* piped through the cbs-01 keep-newest pacer (framePacer.ts), and
|
|
11
|
+
* navigation/lifecycle state surfaced as plain callbacks. All CDP mechanics
|
|
12
|
+
* (target attach, session ids, event wiring) are hidden behind this
|
|
13
|
+
* interface — relayClient.ts (or any future caller) only sees start/
|
|
14
|
+
* navigate/input/back/forward/reload/onFrame/onState/ackFrame/stop.
|
|
15
|
+
*/
|
|
16
|
+
import { ChromiumLaunchHandle } from './chromiumLauncher';
|
|
17
|
+
import { CdpTransportLike } from './cdpClient';
|
|
18
|
+
import { BrowserInputEvent, BrowserSessionState } from './browserFrames';
|
|
19
|
+
/** container -> pane screencast frame, minus the wire envelope (`t`,
|
|
20
|
+
* `sessionId`) — the driver doesn't know the relay session id; the caller
|
|
21
|
+
* (relayClient) stamps those on when forwarding. */
|
|
22
|
+
export interface BrowserDriverFramePayload {
|
|
23
|
+
seq: number;
|
|
24
|
+
data: string;
|
|
25
|
+
w: number;
|
|
26
|
+
h: number;
|
|
27
|
+
}
|
|
28
|
+
/** container -> pane lifecycle/navigation state, minus the wire envelope. */
|
|
29
|
+
export interface BrowserDriverStatePayload {
|
|
30
|
+
state: BrowserSessionState;
|
|
31
|
+
url?: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
reason?: string;
|
|
34
|
+
}
|
|
35
|
+
export interface BrowserDriver {
|
|
36
|
+
/** Launches Chromium (if not already running), attaches to a page
|
|
37
|
+
* target, starts the screencast, and navigates to `url`. */
|
|
38
|
+
start(url: string): Promise<void>;
|
|
39
|
+
/** Navigates the already-open tab to `url` (also what a `browser-open`
|
|
40
|
+
* on an already-open session should call — see relayClient wiring). */
|
|
41
|
+
navigate(url: string): Promise<void>;
|
|
42
|
+
/** Dispatches one input event. Never dropped/paced (unlike frames). */
|
|
43
|
+
input(event: BrowserInputEvent): void;
|
|
44
|
+
back(): Promise<void>;
|
|
45
|
+
forward(): Promise<void>;
|
|
46
|
+
reload(): Promise<void>;
|
|
47
|
+
/** Registers the (single) frame consumer. Frames are keep-newest paced —
|
|
48
|
+
* see ackFrame(). */
|
|
49
|
+
onFrame(cb: (frame: BrowserDriverFramePayload) => void): void;
|
|
50
|
+
/** Registers the (single) lifecycle/navigation state consumer. */
|
|
51
|
+
onState(cb: (state: BrowserDriverStatePayload) => void): void;
|
|
52
|
+
/** Consumer contract for the cbs-01 pacer (framePacer.ts): call once a
|
|
53
|
+
* delivered frame has been handed off downstream. The wire protocol
|
|
54
|
+
* (browserFrames.ts, v1) has no pane-side ack frame yet, so relayClient's
|
|
55
|
+
* interim policy is to call this right after forwarding — true end-to-end
|
|
56
|
+
* backpressure to the pane is a later slice (see PRD module 5). */
|
|
57
|
+
ackFrame(): void;
|
|
58
|
+
/** Kills the Chromium process and tears down the CDP transport, emitting a
|
|
59
|
+
* terminal 'closed' state. Safe to call more than once and safe to call
|
|
60
|
+
* before start() resolves. */
|
|
61
|
+
stop(): void;
|
|
62
|
+
/** cbs-06: same physical teardown as stop() (kills Chromium, tears down
|
|
63
|
+
* CDP — no orphaned process either way), but emits a terminal 'asleep'
|
|
64
|
+
* state instead of 'closed' — intentional and resumable (a later
|
|
65
|
+
* browser-open on this sessionId gets a FRESH driver instance per the
|
|
66
|
+
* relayClient "replace, not navigate" policy; this driver instance is
|
|
67
|
+
* done either way). `reason` rides straight onto the emitted
|
|
68
|
+
* browser-state (default 'sleep'; relayClient passes 'idle-timeout' for
|
|
69
|
+
* the 15-minute auto-close path — see browserFrames.ts's BrowserStateFrame
|
|
70
|
+
* doc comment for the reason-string convention). Safe to call more than
|
|
71
|
+
* once, safe to call before start() resolves, and safe to call after
|
|
72
|
+
* stop() (or vice versa) — only the FIRST of stop()/sleep() wins; the
|
|
73
|
+
* second is a no-op (exactly one terminal browser-state frame per driver
|
|
74
|
+
* lifetime). */
|
|
75
|
+
sleep(reason?: string): void;
|
|
76
|
+
}
|
|
77
|
+
export interface BrowserDriverOpts {
|
|
78
|
+
chromiumPath?: string;
|
|
79
|
+
userDataDir?: string;
|
|
80
|
+
/** Window size AND `Page.startScreencast` maxWidth/maxHeight. Default 1280x800. */
|
|
81
|
+
width?: number;
|
|
82
|
+
height?: number;
|
|
83
|
+
/** JPEG quality for the screencast. Default 60 (PRD: "JPEG quality tuned
|
|
84
|
+
* for text legibility"). */
|
|
85
|
+
quality?: number;
|
|
86
|
+
/** Injectable for tests: defaults to chromiumLauncher.launchChromium. */
|
|
87
|
+
launchChromiumFn?: (opts: {
|
|
88
|
+
chromiumPath?: string;
|
|
89
|
+
userDataDir?: string;
|
|
90
|
+
width?: number;
|
|
91
|
+
height?: number;
|
|
92
|
+
}) => ChromiumLaunchHandle;
|
|
93
|
+
/** Injectable for tests: defaults to a `ws`-backed transport connected to
|
|
94
|
+
* the Chromium DevTools websocket URL. Tests inject an in-process
|
|
95
|
+
* MockCdpEndpoint transport here to avoid a real socket/process. */
|
|
96
|
+
wsFactory?: (url: string) => CdpTransportLike;
|
|
97
|
+
}
|
|
98
|
+
export declare function createBrowserDriver(opts?: BrowserDriverOpts): BrowserDriver;
|