@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.
Files changed (47) hide show
  1. package/dist/__tests__/auth-api-key-format.test.d.ts +1 -0
  2. package/dist/__tests__/auth-api-key-format.test.js +41 -0
  3. package/dist/__tests__/auth-identity.test.d.ts +1 -0
  4. package/dist/__tests__/auth-identity.test.js +46 -0
  5. package/dist/__tests__/auth-login-core.test.d.ts +1 -0
  6. package/dist/__tests__/auth-login-core.test.js +145 -0
  7. package/dist/__tests__/auth.test.js +246 -57
  8. package/dist/__tests__/bridge-bundle-installer.test.js +101 -0
  9. package/dist/__tests__/bridge-context-writer.test.js +81 -1
  10. package/dist/__tests__/bridge-local-context-writer.test.js +11 -2
  11. package/dist/__tests__/client.test.js +26 -4
  12. package/dist/__tests__/config.test.js +36 -4
  13. package/dist/__tests__/openBrowser.test.d.ts +1 -0
  14. package/dist/__tests__/openBrowser.test.js +42 -0
  15. package/dist/__tests__/prompt.test.d.ts +1 -0
  16. package/dist/__tests__/prompt.test.js +49 -0
  17. package/dist/auth/identity.d.ts +22 -0
  18. package/dist/auth/identity.js +20 -0
  19. package/dist/auth/login.d.ts +80 -0
  20. package/dist/auth/login.js +61 -0
  21. package/dist/bridge/apiClient.d.ts +25 -0
  22. package/dist/bridge/apiClient.js +7 -1
  23. package/dist/bridge/bundleInstaller.d.ts +14 -0
  24. package/dist/bridge/bundleInstaller.js +56 -2
  25. package/dist/bridge/contextWriter.d.ts +11 -1
  26. package/dist/bridge/contextWriter.js +52 -8
  27. package/dist/bridge/localContextWriter.d.ts +6 -1
  28. package/dist/bridge/localContextWriter.js +22 -7
  29. package/dist/bridge/pairing.js +7 -1
  30. package/dist/bridge/relayClient.d.ts +9 -0
  31. package/dist/bridge/relayClient.js +25 -0
  32. package/dist/bridge/server.d.ts +6 -0
  33. package/dist/bridge/server.js +8 -0
  34. package/dist/commands/auth.d.ts +17 -0
  35. package/dist/commands/auth.js +202 -27
  36. package/dist/commands/bridge.js +61 -0
  37. package/dist/index.js +0 -0
  38. package/dist/utils/apiKeyFormat.d.ts +9 -0
  39. package/dist/utils/apiKeyFormat.js +26 -0
  40. package/dist/utils/client.js +7 -1
  41. package/dist/utils/config.d.ts +3 -0
  42. package/dist/utils/config.js +28 -2
  43. package/dist/utils/openBrowser.d.ts +16 -0
  44. package/dist/utils/openBrowser.js +30 -0
  45. package/dist/utils/prompt.d.ts +9 -0
  46. package/dist/utils/prompt.js +63 -0
  47. 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';
@@ -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
- return { workspaceRoot, written, skipped };
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
- content: string;
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 hasFileContent = input.content.trim().length > 0;
111
- const sections = [];
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
- sections.push(workGraphPointer_1.WORK_GRAPH_POINTER);
152
+ pointers.push(workGraphPointer_1.WORK_GRAPH_POINTER);
114
153
  if (input.memoryAvailable)
115
- sections.push((0, memoryPointer_1.memorySection)(input.memoryBrief));
116
- if (hasFileContent)
117
- sections.push(input.content);
118
- const content = sections.length > 0 ? sections.join('\n\n') : input.content;
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
- content: string;
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 hasFileContent = input.content.trim().length > 0;
83
- const sections = [];
91
+ const pointers = [];
84
92
  if (input.workGraphAvailable)
85
- sections.push(workGraphPointer_1.WORK_GRAPH_POINTER);
93
+ pointers.push(workGraphPointer_1.WORK_GRAPH_POINTER);
86
94
  if (input.memoryAvailable)
87
- sections.push((0, memoryPointer_1.memorySection)(input.memoryBrief));
88
- if (hasFileContent)
89
- sections.push(input.content);
90
- const content = sections.length > 0 ? sections.join('\n\n') : input.content;
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)
@@ -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
- if (result.status === 'approved') {
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, {
@@ -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;
@@ -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',
@@ -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;