@wenrwa/marketplace-cli 0.5.24 → 0.6.2
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__/auth-api-key-format.test.d.ts +1 -0
- package/dist/__tests__/auth-api-key-format.test.js +41 -0
- package/dist/__tests__/auth-identity.test.d.ts +1 -0
- package/dist/__tests__/auth-identity.test.js +46 -0
- package/dist/__tests__/auth-login-core.test.d.ts +1 -0
- package/dist/__tests__/auth-login-core.test.js +145 -0
- package/dist/__tests__/auth.test.js +246 -57
- package/dist/__tests__/bridge-bundle-installer.test.js +101 -0
- package/dist/__tests__/bridge-context-writer.test.js +81 -1
- package/dist/__tests__/bridge-local-context-writer.test.js +11 -2
- package/dist/__tests__/client.test.js +26 -4
- package/dist/__tests__/config.test.js +36 -4
- package/dist/__tests__/openBrowser.test.d.ts +1 -0
- package/dist/__tests__/openBrowser.test.js +42 -0
- package/dist/__tests__/prompt.test.d.ts +1 -0
- package/dist/__tests__/prompt.test.js +49 -0
- package/dist/auth/identity.d.ts +22 -0
- package/dist/auth/identity.js +20 -0
- package/dist/auth/login.d.ts +80 -0
- package/dist/auth/login.js +61 -0
- package/dist/bridge/apiClient.d.ts +25 -0
- package/dist/bridge/apiClient.js +7 -1
- package/dist/bridge/bundleInstaller.d.ts +14 -0
- package/dist/bridge/bundleInstaller.js +56 -2
- package/dist/bridge/contextWriter.d.ts +11 -1
- package/dist/bridge/contextWriter.js +52 -8
- package/dist/bridge/localContextWriter.d.ts +6 -1
- package/dist/bridge/localContextWriter.js +22 -7
- package/dist/bridge/pairing.js +7 -1
- package/dist/bridge/relayClient.d.ts +9 -0
- package/dist/bridge/relayClient.js +25 -0
- package/dist/bridge/server.d.ts +6 -0
- package/dist/bridge/server.js +8 -0
- package/dist/commands/auth.d.ts +17 -0
- package/dist/commands/auth.js +202 -27
- package/dist/commands/bridge.js +61 -0
- package/dist/index.js +0 -0
- package/dist/utils/apiKeyFormat.d.ts +9 -0
- package/dist/utils/apiKeyFormat.js +26 -0
- package/dist/utils/client.js +7 -1
- package/dist/utils/config.d.ts +3 -0
- package/dist/utils/config.js +28 -2
- package/dist/utils/openBrowser.d.ts +16 -0
- package/dist/utils/openBrowser.js +30 -0
- package/dist/utils/prompt.d.ts +9 -0
- package/dist/utils/prompt.js +63 -0
- package/package.json +1 -1
|
@@ -30,19 +30,44 @@ export interface PairingCodeResponse {
|
|
|
30
30
|
pollToken: string;
|
|
31
31
|
expiresAt: string;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* wha-08: `purpose` is optional and omitted by every bridge caller (pair.ts
|
|
35
|
+
* never passes it), so the request body stays byte-for-byte
|
|
36
|
+
* `{ deviceLabel }` for the bridge — JSON.stringify drops an undefined-
|
|
37
|
+
* valued key. Only `auth/login.ts` (cli_login) passes it explicitly.
|
|
38
|
+
*/
|
|
33
39
|
export declare function requestPairingCode(opts: {
|
|
34
40
|
apiUrl: string;
|
|
35
41
|
deviceLabel: string;
|
|
42
|
+
purpose?: 'bridge' | 'cli_login';
|
|
36
43
|
fetchImpl?: FetchImpl;
|
|
37
44
|
}): Promise<PairingCodeResponse>;
|
|
45
|
+
/**
|
|
46
|
+
* wha-08: widened (additively) to cover the cli_login purpose — a 'denied'
|
|
47
|
+
* status (the human declined in the browser — bridge pairing's poll loop
|
|
48
|
+
* never reaches this branch, see auth/login.ts's header comment) and an
|
|
49
|
+
* 'approved' response that carries a fresh API key instead of a connect
|
|
50
|
+
* token + device id. `purpose` is optional/untyped-through on the bridge
|
|
51
|
+
* variant since the real server has always sent it, but the bridge's own
|
|
52
|
+
* tests stub responses without it — this stays backward compatible.
|
|
53
|
+
*/
|
|
38
54
|
export type PairingPollResponse = {
|
|
39
55
|
success: true;
|
|
40
56
|
status: 'pending';
|
|
41
57
|
} | {
|
|
42
58
|
success: true;
|
|
43
59
|
status: 'approved';
|
|
60
|
+
purpose?: 'bridge';
|
|
44
61
|
connectToken: string;
|
|
45
62
|
deviceId: string;
|
|
63
|
+
} | {
|
|
64
|
+
success: true;
|
|
65
|
+
status: 'approved';
|
|
66
|
+
purpose: 'cli_login';
|
|
67
|
+
apiKey: string;
|
|
68
|
+
} | {
|
|
69
|
+
success: true;
|
|
70
|
+
status: 'denied';
|
|
46
71
|
} | {
|
|
47
72
|
success: true;
|
|
48
73
|
status: 'expired';
|
package/dist/bridge/apiClient.js
CHANGED
|
@@ -69,8 +69,14 @@ async function pairingRequest(apiUrl, path, body, fetchImpl) {
|
|
|
69
69
|
}
|
|
70
70
|
return json;
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* wha-08: `purpose` is optional and omitted by every bridge caller (pair.ts
|
|
74
|
+
* never passes it), so the request body stays byte-for-byte
|
|
75
|
+
* `{ deviceLabel }` for the bridge — JSON.stringify drops an undefined-
|
|
76
|
+
* valued key. Only `auth/login.ts` (cli_login) passes it explicitly.
|
|
77
|
+
*/
|
|
72
78
|
function requestPairingCode(opts) {
|
|
73
|
-
return pairingRequest(opts.apiUrl, '/pairing/codes', { deviceLabel: opts.deviceLabel }, opts.fetchImpl);
|
|
79
|
+
return pairingRequest(opts.apiUrl, '/pairing/codes', { deviceLabel: opts.deviceLabel, purpose: opts.purpose }, opts.fetchImpl);
|
|
74
80
|
}
|
|
75
81
|
function pollPairingCode(opts) {
|
|
76
82
|
return pairingRequest(opts.apiUrl, '/pairing/poll', { pollToken: opts.pollToken }, opts.fetchImpl);
|
|
@@ -17,6 +17,15 @@ export interface BundleInstallerDeps {
|
|
|
17
17
|
mkdirp: (dir: string) => void;
|
|
18
18
|
/** Write (overwrite) a file. */
|
|
19
19
|
writeFile: (filePath: string, content: string) => void;
|
|
20
|
+
/**
|
|
21
|
+
* The three deps a crew SWAP needs. All optional together: absent (any older
|
|
22
|
+
* caller, or a half-wired dep set) → nothing is ever pruned and this module
|
|
23
|
+
* behaves exactly as it did before. Pruning deletes files inside a real
|
|
24
|
+
* checkout, so "not wired" must mean "do nothing", never "guess".
|
|
25
|
+
*/
|
|
26
|
+
listDir?: (dir: string) => string[];
|
|
27
|
+
readFile?: (filePath: string) => string;
|
|
28
|
+
remove?: (filePath: string) => void;
|
|
20
29
|
}
|
|
21
30
|
export type SkipReason = 'unsafe' | 'unmanaged';
|
|
22
31
|
export interface InstallBundleResult {
|
|
@@ -29,6 +38,11 @@ export interface InstallBundleResult {
|
|
|
29
38
|
path: string;
|
|
30
39
|
reason: SkipReason;
|
|
31
40
|
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Crew members removed because this bundle replaced them (relative paths).
|
|
43
|
+
* Reported so an install is auditable rather than silently destructive.
|
|
44
|
+
*/
|
|
45
|
+
pruned: string[];
|
|
32
46
|
}
|
|
33
47
|
/**
|
|
34
48
|
* A workspaceId is the sole dir segment under reposBaseDir — it must be a plain
|
|
@@ -65,6 +65,15 @@ exports.installBundle = installBundle;
|
|
|
65
65
|
* the crew files.
|
|
66
66
|
*/
|
|
67
67
|
const path = __importStar(require("path"));
|
|
68
|
+
/**
|
|
69
|
+
* The marker every forged crew member carries. Its presence is what
|
|
70
|
+
* distinguishes a file this installer put there from one the pilot wrote by
|
|
71
|
+
* hand in the same directory — and the pilot's own subagent must survive a
|
|
72
|
+
* crew swap. If Forge ever stops emitting it, the failure direction is a
|
|
73
|
+
* stale file left behind, never a deleted one.
|
|
74
|
+
*/
|
|
75
|
+
const CREW_FILE_MARKER = '## Prompt Defense Baseline';
|
|
76
|
+
const AGENTS_DIR_REL = '.claude/agents';
|
|
68
77
|
/**
|
|
69
78
|
* A workspaceId is the sole dir segment under reposBaseDir — it must be a plain
|
|
70
79
|
* id, never a path fragment. Reject anything that could climb out of / redirect
|
|
@@ -87,11 +96,12 @@ function isManaged(normalizedRel) {
|
|
|
87
96
|
function installBundle(bundle, deps) {
|
|
88
97
|
const written = [];
|
|
89
98
|
const skipped = [];
|
|
99
|
+
const pruned = [];
|
|
90
100
|
// fcct-27: the id is a path segment ONLY when we join it ourselves. An
|
|
91
101
|
// explicit workspaceRoot never puts it on the path, so there is nothing for
|
|
92
102
|
// it to climb out of — but the guard stays for the joining case.
|
|
93
103
|
if (!deps.workspaceRoot && !isSafeWorkspaceId(bundle.workspaceId)) {
|
|
94
|
-
return { workspaceRoot: null, written, skipped };
|
|
104
|
+
return { workspaceRoot: null, written, skipped, pruned };
|
|
95
105
|
}
|
|
96
106
|
const workspaceRoot = deps.workspaceRoot
|
|
97
107
|
? path.posix.resolve(deps.workspaceRoot)
|
|
@@ -128,5 +138,49 @@ function installBundle(bundle, deps) {
|
|
|
128
138
|
deps.writeFile(target, content);
|
|
129
139
|
written.push(normalized);
|
|
130
140
|
}
|
|
131
|
-
|
|
141
|
+
/*
|
|
142
|
+
A crew SWAP must replace its members, not pile a new crew on top of the old
|
|
143
|
+
one. Before this, installBundle only ever wrote: a live container was found
|
|
144
|
+
holding eight `.claude/agents/*.md` for a five-member crew (and another
|
|
145
|
+
checkout ten), so subagents from a crew the pilot had detached stayed
|
|
146
|
+
callable indefinitely.
|
|
147
|
+
|
|
148
|
+
Every condition below exists to make the failure direction "left a stale
|
|
149
|
+
file" rather than "deleted something of yours":
|
|
150
|
+
- only inside `.claude/agents`, so CLAUDE.md and the rest of `.claude/`
|
|
151
|
+
are untouchable here;
|
|
152
|
+
- only when this bundle installs at least one agent, so a CLAUDE.md-only
|
|
153
|
+
or skills-only bundle can never empty the crew;
|
|
154
|
+
- only files carrying CREW_FILE_MARKER, so a hand-written subagent living
|
|
155
|
+
in the same directory survives;
|
|
156
|
+
- only when the caller wired listDir/readFile/remove;
|
|
157
|
+
- and any error reading the directory abandons pruning entirely — an
|
|
158
|
+
install that wrote its files must not fail because cleanup could not.
|
|
159
|
+
*/
|
|
160
|
+
const installedAgents = new Set(written.filter((rel) => rel.startsWith(`${AGENTS_DIR_REL}/`)));
|
|
161
|
+
if (installedAgents.size > 0 && deps.listDir && deps.readFile && deps.remove) {
|
|
162
|
+
const agentsDir = path.posix.resolve(workspaceRoot, AGENTS_DIR_REL);
|
|
163
|
+
try {
|
|
164
|
+
for (const name of deps.listDir(agentsDir)) {
|
|
165
|
+
if (!name.endsWith('.md'))
|
|
166
|
+
continue;
|
|
167
|
+
const rel = `${AGENTS_DIR_REL}/${name}`;
|
|
168
|
+
if (installedAgents.has(rel))
|
|
169
|
+
continue;
|
|
170
|
+
const full = path.posix.resolve(agentsDir, name);
|
|
171
|
+
// Containment, same discipline as the write path above: a listing that
|
|
172
|
+
// somehow yields a climbing name must not delete outside the directory.
|
|
173
|
+
if (!full.startsWith(agentsDir + '/'))
|
|
174
|
+
continue;
|
|
175
|
+
if (!deps.readFile(full).includes(CREW_FILE_MARKER))
|
|
176
|
+
continue;
|
|
177
|
+
deps.remove(full);
|
|
178
|
+
pruned.push(rel);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Cleanup is best-effort by design; the bundle is already installed.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { workspaceRoot, written, skipped, pruned };
|
|
132
186
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export declare const CONTEXT_START_MARKER = "<!-- HIVEOP:CONTEXT:START -->";
|
|
2
2
|
export declare const CONTEXT_END_MARKER = "<!-- HIVEOP:CONTEXT:END -->";
|
|
3
|
+
export declare const POINTERS_START_MARKER = "<!-- HIVEOP:POINTERS:START -->";
|
|
4
|
+
export declare const POINTERS_END_MARKER = "<!-- HIVEOP:POINTERS:END -->";
|
|
3
5
|
export interface ContextWriterDeps {
|
|
4
6
|
/** Root holding every workspace's checkout, e.g. `/data/repos`. */
|
|
5
7
|
reposBaseDir: string;
|
|
@@ -27,7 +29,8 @@ export interface ContextWriteResult {
|
|
|
27
29
|
*/
|
|
28
30
|
export declare function writeContext(input: {
|
|
29
31
|
workspaceId: string;
|
|
30
|
-
|
|
32
|
+
/** Absent only for a pointers-only refresh (wmr-07). */
|
|
33
|
+
content?: string;
|
|
31
34
|
workGraphAvailable?: boolean;
|
|
32
35
|
/**
|
|
33
36
|
* wmr-01 — whether to carry the workspace-memory guidance. Decided by the
|
|
@@ -42,4 +45,11 @@ export declare function writeContext(input: {
|
|
|
42
45
|
* brief renders the standing instruction alone.
|
|
43
46
|
*/
|
|
44
47
|
memoryBrief?: string | null;
|
|
48
|
+
/**
|
|
49
|
+
* wmr-07 — refresh ONLY the standing pointers, keeping whatever file
|
|
50
|
+
* content the region already holds. Used by the bridge when a session
|
|
51
|
+
* opens, which is the trigger that does not depend on the pilot having
|
|
52
|
+
* selected files or on a browser being connected at all.
|
|
53
|
+
*/
|
|
54
|
+
pointersOnly?: boolean;
|
|
45
55
|
}, deps: ContextWriterDeps): ContextWriteResult;
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.CONTEXT_END_MARKER = exports.CONTEXT_START_MARKER = void 0;
|
|
36
|
+
exports.POINTERS_END_MARKER = exports.POINTERS_START_MARKER = exports.CONTEXT_END_MARKER = exports.CONTEXT_START_MARKER = void 0;
|
|
37
37
|
exports.writeContext = writeContext;
|
|
38
38
|
/**
|
|
39
39
|
* pucc-int-01 (ADR-0032 / ADR-0028) — container context writer.
|
|
@@ -71,6 +71,37 @@ const workGraphPointer_1 = require("./workGraphPointer");
|
|
|
71
71
|
const memoryPointer_1 = require("./memoryPointer");
|
|
72
72
|
exports.CONTEXT_START_MARKER = '<!-- HIVEOP:CONTEXT:START -->';
|
|
73
73
|
exports.CONTEXT_END_MARKER = '<!-- HIVEOP:CONTEXT:END -->';
|
|
74
|
+
/*
|
|
75
|
+
wmr-07 — an inner fence around the standing pointers (work graph, workspace
|
|
76
|
+
memory), INSIDE the managed region.
|
|
77
|
+
|
|
78
|
+
The bridge refreshes those pointers when a session opens, because arena only
|
|
79
|
+
pushes a context-manifest when the pilot has files selected — so waiting for a
|
|
80
|
+
push meant the pointers never arrived at all for most workspaces. But the
|
|
81
|
+
region also holds that pushed file content, and a refresh that rewrote the
|
|
82
|
+
whole region would eat it. This fence is what lets the two be updated
|
|
83
|
+
independently: pointers replaced in place, file content left exactly as it is.
|
|
84
|
+
|
|
85
|
+
A region written before this fence existed has no markers; it is then treated
|
|
86
|
+
as file content and the pointer block is inserted above it, which is the
|
|
87
|
+
correct reading of what it contained.
|
|
88
|
+
*/
|
|
89
|
+
exports.POINTERS_START_MARKER = '<!-- HIVEOP:POINTERS:START -->';
|
|
90
|
+
exports.POINTERS_END_MARKER = '<!-- HIVEOP:POINTERS:END -->';
|
|
91
|
+
/** Split a region body into its pointer block and everything else. */
|
|
92
|
+
function splitRegion(body) {
|
|
93
|
+
const start = body.indexOf(exports.POINTERS_START_MARKER);
|
|
94
|
+
const end = body.indexOf(exports.POINTERS_END_MARKER);
|
|
95
|
+
if (start === -1 || end === -1 || end < start)
|
|
96
|
+
return { pointers: '', rest: body.trim() };
|
|
97
|
+
const rest = (body.slice(0, start) + body.slice(end + exports.POINTERS_END_MARKER.length)).trim();
|
|
98
|
+
return { pointers: body.slice(start, end + exports.POINTERS_END_MARKER.length), rest };
|
|
99
|
+
}
|
|
100
|
+
function pointerBlock(sections) {
|
|
101
|
+
if (sections.length === 0)
|
|
102
|
+
return '';
|
|
103
|
+
return `${exports.POINTERS_START_MARKER}\n${sections.join('\n\n')}\n${exports.POINTERS_END_MARKER}`;
|
|
104
|
+
}
|
|
74
105
|
/** Build the fenced region block for a non-empty body. */
|
|
75
106
|
function regionBlock(content) {
|
|
76
107
|
// Normalize only the region's own trailing newlines so the END marker sits on
|
|
@@ -107,15 +138,28 @@ function writeContext(input, deps) {
|
|
|
107
138
|
// shape once — two independent flags would otherwise need four branches to
|
|
108
139
|
// get the blank lines right, and the empty-content case is the one that
|
|
109
140
|
// decides whether the region survives at all.
|
|
110
|
-
const
|
|
111
|
-
|
|
141
|
+
const existingRegion = (() => {
|
|
142
|
+
if (!existing)
|
|
143
|
+
return '';
|
|
144
|
+
const s0 = existing.indexOf(exports.CONTEXT_START_MARKER);
|
|
145
|
+
const e0 = existing.indexOf(exports.CONTEXT_END_MARKER);
|
|
146
|
+
if (s0 === -1 || e0 === -1 || e0 < s0)
|
|
147
|
+
return '';
|
|
148
|
+
return existing.slice(s0 + exports.CONTEXT_START_MARKER.length, e0).trim();
|
|
149
|
+
})();
|
|
150
|
+
const pointers = [];
|
|
112
151
|
if (input.workGraphAvailable)
|
|
113
|
-
|
|
152
|
+
pointers.push(workGraphPointer_1.WORK_GRAPH_POINTER);
|
|
114
153
|
if (input.memoryAvailable)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
154
|
+
pointers.push((0, memoryPointer_1.memorySection)(input.memoryBrief));
|
|
155
|
+
// wmr-07: a pointers-only refresh keeps the region's existing file content
|
|
156
|
+
// rather than replacing it with the (absent) `content` argument.
|
|
157
|
+
const fileContent = input.pointersOnly
|
|
158
|
+
? splitRegion(existingRegion).rest
|
|
159
|
+
: (input.content ?? '').trim();
|
|
160
|
+
const block = pointerBlock(pointers);
|
|
161
|
+
const parts = [block, fileContent].filter((part) => part.length > 0);
|
|
162
|
+
const content = parts.join('\n\n');
|
|
119
163
|
const isEmpty = content.trim().length === 0;
|
|
120
164
|
// Locate any existing managed region.
|
|
121
165
|
const startIdx = existing ? existing.indexOf(exports.CONTEXT_START_MARKER) : -1;
|
|
@@ -26,7 +26,8 @@ export interface LocalContextWriteResult {
|
|
|
26
26
|
*/
|
|
27
27
|
export declare function writeLocalContext(input: {
|
|
28
28
|
projectRoot: string;
|
|
29
|
-
|
|
29
|
+
/** Absent only for a pointers-only refresh (wmr-07). */
|
|
30
|
+
content?: string;
|
|
30
31
|
workGraphAvailable?: boolean;
|
|
31
32
|
/** wmr-04 — two-lane parity with contextWriter.ts. Gated by the caller on
|
|
32
33
|
* the local lane's own honest signal (did THIS bridge run register the
|
|
@@ -35,4 +36,8 @@ export declare function writeLocalContext(input: {
|
|
|
35
36
|
/** wmr-04 — this workspace's composed brief, or null when the fetch failed
|
|
36
37
|
* or there is nothing yet. Never blocks the write (PRD D2). */
|
|
37
38
|
memoryBrief?: string | null;
|
|
39
|
+
/** wmr-07 — refresh only the standing pointers, keeping the pushed file
|
|
40
|
+
* content. The local twin of contextWriter's flag; same reason (the
|
|
41
|
+
* manifest frame only arrives when the pilot has files selected). */
|
|
42
|
+
pointersOnly?: boolean;
|
|
38
43
|
}, deps: LocalContextWriterDeps): LocalContextWriteResult;
|
|
@@ -56,6 +56,15 @@ exports.writeLocalContext = writeLocalContext;
|
|
|
56
56
|
const path = __importStar(require("path"));
|
|
57
57
|
const workGraphPointer_1 = require("./workGraphPointer");
|
|
58
58
|
const memoryPointer_1 = require("./memoryPointer");
|
|
59
|
+
const contextWriter_1 = require("./contextWriter");
|
|
60
|
+
/** Split a bridge-owned context file into its pointer block and the rest. */
|
|
61
|
+
function splitPointers(body) {
|
|
62
|
+
const start = body.indexOf(contextWriter_1.POINTERS_START_MARKER);
|
|
63
|
+
const end = body.indexOf(contextWriter_1.POINTERS_END_MARKER);
|
|
64
|
+
if (start === -1 || end === -1 || end < start)
|
|
65
|
+
return { rest: body.trim() };
|
|
66
|
+
return { rest: (body.slice(0, start) + body.slice(end + contextWriter_1.POINTERS_END_MARKER.length)).trim() };
|
|
67
|
+
}
|
|
59
68
|
exports.LOCAL_CONTEXT_DIR = '.hiveop';
|
|
60
69
|
exports.LOCAL_CONTEXT_FILENAME = 'context.md';
|
|
61
70
|
exports.LOCAL_CONTEXT_RELATIVE_PATH = `${exports.LOCAL_CONTEXT_DIR}/${exports.LOCAL_CONTEXT_FILENAME}`;
|
|
@@ -79,15 +88,21 @@ function writeLocalContext(input, deps) {
|
|
|
79
88
|
// the same shape contextWriter.ts uses, because the two lanes diverging on
|
|
80
89
|
// this would be a two-lane bug nobody would notice until a local pilot
|
|
81
90
|
// reported missing memory.
|
|
82
|
-
const
|
|
83
|
-
const sections = [];
|
|
91
|
+
const pointers = [];
|
|
84
92
|
if (input.workGraphAvailable)
|
|
85
|
-
|
|
93
|
+
pointers.push(workGraphPointer_1.WORK_GRAPH_POINTER);
|
|
86
94
|
if (input.memoryAvailable)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
95
|
+
pointers.push((0, memoryPointer_1.memorySection)(input.memoryBrief));
|
|
96
|
+
// wmr-07 — the whole local file is bridge-owned, so "preserve the pushed
|
|
97
|
+
// content" means keeping what is already on disk below the pointer fence.
|
|
98
|
+
const existingBody = existed ? deps.readFile(contextPath) : '';
|
|
99
|
+
const fileContent = input.pointersOnly
|
|
100
|
+
? splitPointers(existingBody).rest
|
|
101
|
+
: (input.content ?? '').trim();
|
|
102
|
+
const block = pointers.length > 0
|
|
103
|
+
? `${contextWriter_1.POINTERS_START_MARKER}\n${pointers.join('\n\n')}\n${contextWriter_1.POINTERS_END_MARKER}`
|
|
104
|
+
: '';
|
|
105
|
+
const content = [block, fileContent].filter((part) => part.length > 0).join('\n\n');
|
|
91
106
|
const isEmpty = content.trim().length === 0;
|
|
92
107
|
if (isEmpty) {
|
|
93
108
|
if (!existed)
|
package/dist/bridge/pairing.js
CHANGED
|
@@ -18,7 +18,13 @@ async function runPairingPoll(deps) {
|
|
|
18
18
|
const result = await deps.poll();
|
|
19
19
|
if (deps.isCancelled())
|
|
20
20
|
return { outcome: 'cancelled' };
|
|
21
|
-
|
|
21
|
+
// wha-08: PairingPollResponse's 'approved' status now also covers
|
|
22
|
+
// cli_login (auth/login.ts), which carries an apiKey instead of a
|
|
23
|
+
// connectToken/deviceId — `purpose !== 'cli_login'` is a type-only
|
|
24
|
+
// narrowing, never a behavior change: a bridge pollToken never yields a
|
|
25
|
+
// cli_login-shaped response, so this condition is always true for any
|
|
26
|
+
// real bridge poll.
|
|
27
|
+
if (result.status === 'approved' && result.purpose !== 'cli_login') {
|
|
22
28
|
return { outcome: 'approved', connectToken: result.connectToken, deviceId: result.deviceId };
|
|
23
29
|
}
|
|
24
30
|
if (result.status === 'expired') {
|
|
@@ -221,6 +221,15 @@ export interface RelayClientOpts {
|
|
|
221
221
|
* error (legacy / local bridge has no workspace checkouts). This opens NO PTY
|
|
222
222
|
* and does NOT feed Claude Code via keystrokes — the context arrives as a FILE.
|
|
223
223
|
*/
|
|
224
|
+
/**
|
|
225
|
+
* wmr-07 — refresh this workspace's standing pointers (work graph, memory
|
|
226
|
+
* brief) without touching its pushed file content. Called when a session
|
|
227
|
+
* opens, because the context-manifest frame only arrives when the pilot has
|
|
228
|
+
* files selected. Absent → nothing happens, exactly as before.
|
|
229
|
+
*/
|
|
230
|
+
refreshContextPointers?: (args: {
|
|
231
|
+
workspaceId: string;
|
|
232
|
+
}) => void | Promise<void>;
|
|
224
233
|
writeContext?: (manifest: {
|
|
225
234
|
workspaceId: string;
|
|
226
235
|
content: string;
|
|
@@ -1009,6 +1009,31 @@ function startRelayClient(opts) {
|
|
|
1009
1009
|
relayOwned.set(sessionId, { workspaceId });
|
|
1010
1010
|
// tchp-21 — best-effort, fire-and-forget: never blocks `opened`.
|
|
1011
1011
|
maybeCreateSessionAnchor(sessionId, cwd);
|
|
1012
|
+
/*
|
|
1013
|
+
wmr-07 — refresh the standing pointers (work graph, workspace
|
|
1014
|
+
memory + its brief) for this workspace now that a session
|
|
1015
|
+
exists.
|
|
1016
|
+
|
|
1017
|
+
This is the trigger the whole delivery was missing. wmr-01..04
|
|
1018
|
+
rode the context-manifest frame on the assumption it fires when
|
|
1019
|
+
a terminal attaches; it does not — arena only pushes one when
|
|
1020
|
+
the pilot has FILES SELECTED, so for an ordinary workspace the
|
|
1021
|
+
region was never written and neither the memory brief nor the
|
|
1022
|
+
work-graph pointer (shipped months earlier) ever arrived.
|
|
1023
|
+
Confirmed on a live container: CLAUDE.md untouched for 20 hours
|
|
1024
|
+
with neither present.
|
|
1025
|
+
|
|
1026
|
+
Doing it here needs no browser, no selection, and no arena at
|
|
1027
|
+
all — only a session opening, which is exactly when an agent is
|
|
1028
|
+
about to read the file. Pointers-only, so the pilot's pushed
|
|
1029
|
+
file content is preserved. Fire-and-forget: this must never
|
|
1030
|
+
delay or fail an `opened`.
|
|
1031
|
+
*/
|
|
1032
|
+
if (workspaceId && opts.refreshContextPointers) {
|
|
1033
|
+
void Promise.resolve(opts.refreshContextPointers({ workspaceId })).catch((err) => {
|
|
1034
|
+
console.error('[bridge] context pointer refresh failed (non-fatal):', err);
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1012
1037
|
attachTo(sessionId)
|
|
1013
1038
|
.then(() => {
|
|
1014
1039
|
send(socket, {
|
package/dist/bridge/server.d.ts
CHANGED
|
@@ -200,6 +200,12 @@ export interface BridgeServerOpts {
|
|
|
200
200
|
/** wmr-04 — may be async: the implementation fetches this workspace's memory
|
|
201
201
|
* brief before writing. The frame handler resolves it rather than becoming
|
|
202
202
|
* async itself, so no other frame type's ordering changes. */
|
|
203
|
+
/** wmr-07 — the local twin of the hosted refreshContextPointers: write this
|
|
204
|
+
* workspace's standing pointers when a session opens, without waiting for a
|
|
205
|
+
* context-manifest that only arrives when files are selected. */
|
|
206
|
+
refreshLocalContextPointers?: (args: {
|
|
207
|
+
workspaceId: string;
|
|
208
|
+
}) => void | Promise<void>;
|
|
203
209
|
writeLocalContext?: (params: {
|
|
204
210
|
workspaceId: string;
|
|
205
211
|
content: string;
|
package/dist/bridge/server.js
CHANGED
|
@@ -766,6 +766,14 @@ function startBridgeServer(opts) {
|
|
|
766
766
|
send(ws, { t: 'error', message });
|
|
767
767
|
break;
|
|
768
768
|
}
|
|
769
|
+
// wmr-07 — same trigger as the hosted lane: the pointers are
|
|
770
|
+
// refreshed when a session opens, not when a manifest happens to
|
|
771
|
+
// be pushed. Fire-and-forget; never delays or fails `opened`.
|
|
772
|
+
if (frame.workspaceId && opts.refreshLocalContextPointers) {
|
|
773
|
+
void Promise.resolve(opts.refreshLocalContextPointers({ workspaceId: frame.workspaceId })).catch((err) => {
|
|
774
|
+
console.error('[bridge] local context pointer refresh failed (non-fatal):', err);
|
|
775
|
+
});
|
|
776
|
+
}
|
|
769
777
|
attachTo(sessionId).then(() => {
|
|
770
778
|
send(ws, {
|
|
771
779
|
t: 'opened',
|
package/dist/commands/auth.d.ts
CHANGED
|
@@ -1,2 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wha-08 (docs/prd/retire-wallet-header-auth.md, docs/CONTEXT-identity.md) —
|
|
3
|
+
* `wen-market auth login` / `auth logout` / `auth whoami`.
|
|
4
|
+
*
|
|
5
|
+
* `login` runs the device-code flow (lws-14's mechanism, purpose=cli_login —
|
|
6
|
+
* see auth/login.ts): a code is minted and printed with the approval URL
|
|
7
|
+
* (https://marketplace.wenrwa.com/settings/cli-login?code=<CODE>), the
|
|
8
|
+
* browser is opened as best-effort convenience, and the CLI polls until a
|
|
9
|
+
* human approves or denies it there. `--api-key` skips all of that for CI:
|
|
10
|
+
* an existing key is verified with one authenticated call and saved
|
|
11
|
+
* directly. Either way the CLI authenticates as an API key (X-API-Key) —
|
|
12
|
+
* never the retired X-Wallet-Pubkey header (see utils/client.ts).
|
|
13
|
+
*
|
|
14
|
+
* `logout` revokes the stored key server-side (DELETE /keys/:id — see
|
|
15
|
+
* ApiKeyController#revoke's wha-08 self-revoke-only guard) before deleting
|
|
16
|
+
* it locally, so a copied config file stops working too.
|
|
17
|
+
*/
|
|
1
18
|
import { Command } from 'commander';
|
|
2
19
|
export declare const authCommand: Command;
|