@wenrwa/marketplace-cli 0.5.23 → 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 (54) 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 +168 -1
  10. package/dist/__tests__/bridge-local-context-writer.test.js +51 -2
  11. package/dist/__tests__/bridge-memory-brief.test.d.ts +1 -0
  12. package/dist/__tests__/bridge-memory-brief.test.js +107 -0
  13. package/dist/__tests__/bridge-memory-pointer.test.d.ts +1 -0
  14. package/dist/__tests__/bridge-memory-pointer.test.js +92 -0
  15. package/dist/__tests__/client.test.js +26 -4
  16. package/dist/__tests__/config.test.js +36 -4
  17. package/dist/__tests__/openBrowser.test.d.ts +1 -0
  18. package/dist/__tests__/openBrowser.test.js +42 -0
  19. package/dist/__tests__/prompt.test.d.ts +1 -0
  20. package/dist/__tests__/prompt.test.js +49 -0
  21. package/dist/auth/identity.d.ts +22 -0
  22. package/dist/auth/identity.js +20 -0
  23. package/dist/auth/login.d.ts +80 -0
  24. package/dist/auth/login.js +61 -0
  25. package/dist/bridge/apiClient.d.ts +25 -0
  26. package/dist/bridge/apiClient.js +7 -1
  27. package/dist/bridge/bundleInstaller.d.ts +14 -0
  28. package/dist/bridge/bundleInstaller.js +56 -2
  29. package/dist/bridge/contextWriter.d.ts +24 -1
  30. package/dist/bridge/contextWriter.js +61 -5
  31. package/dist/bridge/localContextWriter.d.ts +13 -1
  32. package/dist/bridge/localContextWriter.js +31 -4
  33. package/dist/bridge/memoryBrief.d.ts +41 -0
  34. package/dist/bridge/memoryBrief.js +63 -0
  35. package/dist/bridge/memoryPointer.d.ts +49 -0
  36. package/dist/bridge/memoryPointer.js +62 -0
  37. package/dist/bridge/pairing.js +7 -1
  38. package/dist/bridge/relayClient.d.ts +20 -1
  39. package/dist/bridge/relayClient.js +45 -6
  40. package/dist/bridge/server.d.ts +10 -1
  41. package/dist/bridge/server.js +29 -10
  42. package/dist/commands/auth.d.ts +17 -0
  43. package/dist/commands/auth.js +202 -27
  44. package/dist/commands/bridge.js +82 -2
  45. package/dist/utils/apiKeyFormat.d.ts +9 -0
  46. package/dist/utils/apiKeyFormat.js +26 -0
  47. package/dist/utils/client.js +7 -1
  48. package/dist/utils/config.d.ts +3 -0
  49. package/dist/utils/config.js +28 -2
  50. package/dist/utils/openBrowser.d.ts +16 -0
  51. package/dist/utils/openBrowser.js +30 -0
  52. package/dist/utils/prompt.d.ts +9 -0
  53. package/dist/utils/prompt.js +63 -0
  54. package/package.json +2 -2
@@ -1,37 +1,129 @@
1
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
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.authCommand = void 0;
40
+ /**
41
+ * wha-08 (docs/prd/retire-wallet-header-auth.md, docs/CONTEXT-identity.md) —
42
+ * `wen-market auth login` / `auth logout` / `auth whoami`.
43
+ *
44
+ * `login` runs the device-code flow (lws-14's mechanism, purpose=cli_login —
45
+ * see auth/login.ts): a code is minted and printed with the approval URL
46
+ * (https://marketplace.wenrwa.com/settings/cli-login?code=<CODE>), the
47
+ * browser is opened as best-effort convenience, and the CLI polls until a
48
+ * human approves or denies it there. `--api-key` skips all of that for CI:
49
+ * an existing key is verified with one authenticated call and saved
50
+ * directly. Either way the CLI authenticates as an API key (X-API-Key) —
51
+ * never the retired X-Wallet-Pubkey header (see utils/client.ts).
52
+ *
53
+ * `logout` revokes the stored key server-side (DELETE /keys/:id — see
54
+ * ApiKeyController#revoke's wha-08 self-revoke-only guard) before deleting
55
+ * it locally, so a copied config file stops working too.
56
+ */
7
57
  const commander_1 = require("commander");
58
+ const os = __importStar(require("os"));
8
59
  const chalk_1 = __importDefault(require("chalk"));
9
60
  const config_1 = require("../utils/config");
61
+ const client_1 = require("../utils/client");
62
+ const apiClient_1 = require("../bridge/apiClient");
63
+ const login_1 = require("../auth/login");
64
+ const identity_1 = require("../auth/identity");
65
+ const prompt_1 = require("../utils/prompt");
66
+ const openBrowser_1 = require("../utils/openBrowser");
67
+ const apiKeyFormat_1 = require("../utils/apiKeyFormat");
10
68
  const format_1 = require("../utils/format");
69
+ const WEB_URL = process.env.WENRWA_WEB_URL || 'https://marketplace.wenrwa.com';
70
+ const SETTINGS_KEYS_URL = `${WEB_URL}/settings`;
11
71
  exports.authCommand = new commander_1.Command('auth').description('Authentication commands');
12
72
  exports.authCommand
13
73
  .command('login')
14
- .description('Authenticate with the marketplace')
15
- .requiredOption('--api-key <key>', 'API key or wallet pubkey')
74
+ .description('Sign in via your browser, or provide a CI key with --api-key')
75
+ .option('--api-key <key>', 'Use an existing API key non-interactively (for CI)')
16
76
  .option('--api-url <url>', 'Marketplace API URL')
17
77
  .action(async (opts) => {
18
78
  const apiUrl = opts.apiUrl || (0, config_1.getApiUrl)();
79
+ const existing = (0, config_1.loadConfig)();
80
+ if (existing) {
81
+ const proceed = await confirmReplaceExistingLogin(existing);
82
+ if (!proceed)
83
+ return;
84
+ }
85
+ if (opts.apiKey) {
86
+ await loginWithApiKey(opts.apiKey, apiUrl);
87
+ return;
88
+ }
89
+ await runDeviceLogin(apiUrl);
90
+ });
91
+ exports.authCommand
92
+ .command('logout')
93
+ .description('Sign out and revoke the stored API key')
94
+ .action(async () => {
95
+ const config = (0, config_1.loadConfig)();
96
+ if (!config) {
97
+ (0, format_1.info)('Not signed in.');
98
+ return;
99
+ }
100
+ let revokedServerSide = false;
101
+ let warning = null;
19
102
  try {
20
- const res = await fetch(`${apiUrl}/agents/${opts.apiKey}`);
21
- if (res.ok) {
22
- (0, format_1.info)(`Authenticated as agent: ${opts.apiKey}`);
103
+ const client = (0, client_1.getClient)();
104
+ const data = await client.get('/keys');
105
+ const prefix = config.apiKey.slice(0, 16);
106
+ const match = (data.keys || []).find((k) => k.keyPrefix === prefix);
107
+ if (match) {
108
+ await client.del(`/keys/${match.id}`);
109
+ revokedServerSide = true;
23
110
  }
24
111
  else {
25
- (0, format_1.info)('API key stored (agent not found — may be a poster or new user)');
112
+ warning = 'Could not find this key on the server (it may already be revoked).';
26
113
  }
27
114
  }
28
- catch {
29
- (0, format_1.info)('API key stored (could not reach server — will validate on next request)');
115
+ catch (err) {
116
+ warning = `Could not revoke the key on the server: ${err?.message || err}`;
117
+ }
118
+ // Local credentials come out regardless of the server outcome — a stale
119
+ // or unreachable server must never leave the CLI looking "signed in".
120
+ (0, config_1.clearConfig)();
121
+ if (revokedServerSide) {
122
+ console.log(chalk_1.default.green('✓ Signed out. API key revoked.'));
123
+ }
124
+ else {
125
+ (0, format_1.error)(`${warning} Removed the key locally — it may still work server-side until it expires or is revoked another way.`);
30
126
  }
31
- (0, config_1.saveConfig)({ apiKey: opts.apiKey, apiUrl });
32
- (0, format_1.success)(`Logged in. Config saved.`);
33
- console.log(` API URL: ${chalk_1.default.dim(apiUrl)}`);
34
- console.log(` Key: ${chalk_1.default.dim(opts.apiKey.slice(0, 8))}...`);
35
127
  });
36
128
  exports.authCommand
37
129
  .command('whoami')
@@ -39,25 +131,108 @@ exports.authCommand
39
131
  .action(async () => {
40
132
  const config = (0, config_1.loadConfig)();
41
133
  if (!config) {
42
- (0, format_1.error)('Not logged in. Run: wen-market auth login --api-key <key>');
134
+ (0, format_1.error)('Not signed in. Run: wen-market auth login');
43
135
  process.exit(1);
136
+ return;
44
137
  }
45
138
  console.log(chalk_1.default.bold('Current session:'));
46
139
  console.log(` API URL: ${config.apiUrl}`);
47
- console.log(` Key: ${config.apiKey}`);
48
- try {
49
- const res = await fetch(`${config.apiUrl}/agents/${config.apiKey}`);
50
- if (res.ok) {
51
- const data = await res.json();
52
- const agent = data.agent;
53
- if (agent) {
54
- console.log(` Name: ${agent.name || 'N/A'}`);
55
- console.log(` Status: ${agent.status || 'active'}`);
56
- console.log(` Rep: ${agent.weighted_reputation ?? 'N/A'}`);
57
- }
58
- }
140
+ console.log(` Key: ${(0, apiKeyFormat_1.shortenKey)(config.apiKey)}`);
141
+ const identity = await (0, identity_1.resolveIdentity)({ apiUrl: config.apiUrl, apiKey: config.apiKey });
142
+ if (identity.ok) {
143
+ if (identity.name)
144
+ console.log(` Name: ${identity.name}`);
59
145
  }
60
- catch {
61
- (0, format_1.info)('Could not reach API to verify identity');
146
+ else {
147
+ (0, format_1.info)(`Could not verify identity: ${identity.message}`);
62
148
  }
63
149
  });
150
+ // ── Already-signed-in gate ────────────────────────────────────────────
151
+ async function confirmReplaceExistingLogin(existing) {
152
+ const label = (0, apiKeyFormat_1.shortenKey)(existing.apiKey);
153
+ if (!(0, prompt_1.isInteractive)()) {
154
+ (0, format_1.error)(`Already signed in (${label}). Run this in an interactive terminal to confirm, or \`wen-market auth logout\` first.`);
155
+ return false;
156
+ }
157
+ const yes = await (0, prompt_1.confirm)(`Already signed in as ${label}. Sign in again? (y/N) `, false);
158
+ if (!yes) {
159
+ (0, format_1.info)('Cancelled — staying signed in.');
160
+ }
161
+ return yes;
162
+ }
163
+ // ── --api-key (CI) path ─────────────────────────────────────────────────
164
+ async function loginWithApiKey(key, apiUrl) {
165
+ if ((0, apiKeyFormat_1.looksLikeWalletPubkey)(key)) {
166
+ (0, format_1.error)(`That's a wallet address, not an API key. Run \`wen-market auth login\` or create a key at ${SETTINGS_KEYS_URL}`);
167
+ process.exit(1);
168
+ return;
169
+ }
170
+ if (!(0, apiKeyFormat_1.isRealApiKey)(key)) {
171
+ (0, format_1.error)(`Not a valid API key (expected one starting with wm_sk_). Run \`wen-market auth login\` or create a key at ${SETTINGS_KEYS_URL}`);
172
+ process.exit(1);
173
+ return;
174
+ }
175
+ const identity = await (0, identity_1.resolveIdentity)({ apiUrl, apiKey: key });
176
+ if (!identity.ok) {
177
+ (0, format_1.error)(`Could not verify this API key: ${identity.message}`);
178
+ process.exit(1);
179
+ return;
180
+ }
181
+ (0, config_1.saveConfig)({ apiKey: key, apiUrl });
182
+ console.log(chalk_1.default.green(`✓ Signed in as ${identity.name || (0, apiKeyFormat_1.shortenKey)(key)}`));
183
+ }
184
+ // ── Browser device flow ─────────────────────────────────────────────────
185
+ async function runDeviceLogin(apiUrl) {
186
+ let cancelled = false;
187
+ const onSigint = () => {
188
+ cancelled = true;
189
+ console.log(chalk_1.default.yellow('\nCancelling — finishing the in-flight request…'));
190
+ };
191
+ process.on('SIGINT', onSigint);
192
+ try {
193
+ const outcome = await (0, login_1.loginCore)({ apiUrl }, {
194
+ requestPairingCode: apiClient_1.requestPairingCode,
195
+ pollPairingCode: apiClient_1.pollPairingCode,
196
+ hostname: () => os.hostname(),
197
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
198
+ now: () => Date.now(),
199
+ isCancelled: () => cancelled,
200
+ onCodeReady: ({ code, expiresAt }) => {
201
+ const url = `${WEB_URL}/settings/cli-login?code=${code}`;
202
+ const minutesLeft = Math.max(1, Math.round((new Date(expiresAt).getTime() - Date.now()) / 60000));
203
+ console.log('');
204
+ console.log(chalk_1.default.bold.cyan(` ${code}`));
205
+ console.log('');
206
+ console.log(chalk_1.default.dim(`Approve at ${chalk_1.default.underline(url)}`));
207
+ console.log(chalk_1.default.dim(`Expires in ~${minutesLeft} min. Waiting for approval… (Ctrl+C to cancel)`));
208
+ (0, openBrowser_1.openBrowser)(url);
209
+ },
210
+ });
211
+ await reportLoginOutcome(outcome, apiUrl);
212
+ }
213
+ finally {
214
+ process.off('SIGINT', onSigint);
215
+ }
216
+ }
217
+ async function reportLoginOutcome(outcome, apiUrl) {
218
+ switch (outcome.result.outcome) {
219
+ case 'approved': {
220
+ (0, config_1.saveConfig)({ apiKey: outcome.result.apiKey, apiUrl });
221
+ const identity = await (0, identity_1.resolveIdentity)({ apiUrl, apiKey: outcome.result.apiKey });
222
+ const name = identity.ok && identity.name ? identity.name : (0, apiKeyFormat_1.shortenKey)(outcome.result.apiKey);
223
+ console.log(chalk_1.default.green(`✓ Signed in as ${name}`));
224
+ return;
225
+ }
226
+ case 'denied':
227
+ (0, format_1.error)('Login denied in the browser.');
228
+ return;
229
+ case 'expired':
230
+ case 'timeout':
231
+ (0, format_1.error)('Code expired — run `wen-market auth login` again.');
232
+ return;
233
+ case 'cancelled':
234
+ default:
235
+ console.log(chalk_1.default.yellow('Login cancelled — no changes made.'));
236
+ return;
237
+ }
238
+ }
@@ -73,6 +73,7 @@ const repoCheckout_1 = require("../bridge/repoCheckout");
73
73
  const bundleInstaller_1 = require("../bridge/bundleInstaller");
74
74
  const agentLaunch_1 = require("../bridge/agentLaunch");
75
75
  const contextWriter_1 = require("../bridge/contextWriter");
76
+ const memoryBrief_1 = require("../bridge/memoryBrief");
76
77
  const containerFileRelay_1 = require("../bridge/containerFileRelay");
77
78
  const claudeTranscriptReader_1 = require("../bridge/claudeTranscriptReader");
78
79
  const config_2 = require("../utils/config");
@@ -704,9 +705,17 @@ exports.bridgeCommand = new commander_1.Command('bridge')
704
705
  reposBaseDir,
705
706
  mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
706
707
  writeFile: (p, content) => fs.writeFileSync(p, content),
708
+ // Crew swap: replace the previous crew's members instead of
709
+ // piling on top of them. The installer decides WHAT may go (only
710
+ // marker-carrying files in .claude/agents that this bundle does
711
+ // not include); these three only give it eyes and hands.
712
+ listDir: (dir) => (fs.existsSync(dir) ? fs.readdirSync(dir) : []),
713
+ readFile: (p) => fs.readFileSync(p, 'utf-8'),
714
+ remove: (p) => fs.rmSync(p, { force: true }),
707
715
  });
708
716
  console.log(chalk_1.default.green(`✓ Installed crew bundle → workspace ${workspaceId} (${result.written.length} files` +
709
717
  (result.skipped.length ? `, ${result.skipped.length} skipped` : '') +
718
+ (result.pruned.length ? `, ${result.pruned.length} stale crew member(s) removed` : '') +
710
719
  ')'));
711
720
  return result;
712
721
  },
@@ -714,7 +723,44 @@ exports.bridgeCommand = new commander_1.Command('bridge')
714
723
  // into a fenced managed region of /data/repos/<workspaceId>/CLAUDE.md so
715
724
  // Claude Code reads it natively next turn. A FILE, never PTY input;
716
725
  // path-safe + idempotent; empty content removes the region.
717
- writeContext: ({ workspaceId, content }) => {
726
+ /*
727
+ wmr-07 — the pointer refresh the hosted bridge fires when a session
728
+ opens. Same writer, same gate, same fetch as the manifest path below;
729
+ the only difference is `pointersOnly`, which keeps whatever file
730
+ content the region already holds.
731
+
732
+ It exists because the manifest frame is not a reliable trigger: arena
733
+ only sends one when the pilot has files selected, so most workspaces
734
+ never got the pointers at all.
735
+ */
736
+ refreshContextPointers: async ({ workspaceId }) => {
737
+ const memoryAvailable = Boolean(process.env.WENRWA_CONNECT_TOKEN);
738
+ const memoryBrief = memoryAvailable ? await (0, memoryBrief_1.fetchMemoryBrief)(workspaceId) : null;
739
+ const result = (0, contextWriter_1.writeContext)({
740
+ workspaceId,
741
+ pointersOnly: true,
742
+ workGraphAvailable: Boolean(process.env.WENRWA_CONNECT_TOKEN),
743
+ memoryAvailable,
744
+ memoryBrief,
745
+ }, {
746
+ reposBaseDir,
747
+ exists: (p) => fs.existsSync(p),
748
+ readFile: (p) => fs.readFileSync(p, 'utf-8'),
749
+ writeFile: (p, content) => fs.writeFileSync(p, content),
750
+ mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
751
+ });
752
+ if (result.action !== 'noop') {
753
+ console.log(chalk_1.default.green(`✓ Context pointers ${result.action} → workspace ${workspaceId}`));
754
+ }
755
+ },
756
+ writeContext: async ({ workspaceId, content }) => {
757
+ // wmr-03 — the workspace's memory brief, fetched at manifest time so
758
+ // the agent has it in context from its next turn. Never blocks the
759
+ // write: fetchMemoryBrief resolves null on any failure (PRD D2), and
760
+ // carries its own bounded timeout so a hanging backend cannot stall a
761
+ // terminal opening.
762
+ const memoryAvailable = Boolean(process.env.WENRWA_CONNECT_TOKEN);
763
+ const memoryBrief = memoryAvailable ? await (0, memoryBrief_1.fetchMemoryBrief)(workspaceId) : null;
718
764
  const result = (0, contextWriter_1.writeContext)({
719
765
  workspaceId,
720
766
  content,
@@ -724,6 +770,10 @@ exports.bridgeCommand = new commander_1.Command('bridge')
724
770
  // means the MCP tools this pointer names are not registered here —
725
771
  // pointing at tools that do not exist is worse than silence.
726
772
  workGraphAvailable: Boolean(process.env.WENRWA_CONNECT_TOKEN),
773
+ // wmr-01 — same gate, same reason: the token is what proves the
774
+ // MCP tools named by the memory pointer are registered here.
775
+ memoryAvailable,
776
+ memoryBrief,
727
777
  }, {
728
778
  reposBaseDir,
729
779
  exists: (p) => fs.existsSync(p),
@@ -1019,10 +1069,30 @@ exports.bridgeCommand = new commander_1.Command('bridge')
1019
1069
  // lws-08 (PRD D9) — a context-manifest frame writes ONLY
1020
1070
  // `.hiveop/context.md` under the resolved local workspace's project
1021
1071
  // root; unknown workspaceId → null (server.ts errors the frame).
1022
- writeLocalContext: ({ workspaceId, content }) => {
1072
+ // wmr-07 — the local twin of refreshContextPointers above.
1073
+ refreshLocalContextPointers: async ({ workspaceId }) => {
1074
+ const entry = (0, folderRegistry_1.findEntryByWorkspaceId)((0, folderRegistry_1.loadFolderRegistry)(), workspaceId);
1075
+ if (!entry)
1076
+ return;
1077
+ const memoryAvailable = mcp.action !== 'claude-not-found';
1078
+ const memoryBrief = memoryAvailable ? await (0, memoryBrief_1.fetchMemoryBrief)(workspaceId) : null;
1079
+ (0, localContextWriter_1.writeLocalContext)({
1080
+ projectRoot: entry.path,
1081
+ pointersOnly: true,
1082
+ workGraphAvailable: mcp.action !== 'claude-not-found',
1083
+ memoryAvailable,
1084
+ memoryBrief,
1085
+ }, realLocalContextWriterDeps());
1086
+ },
1087
+ writeLocalContext: async ({ workspaceId, content }) => {
1023
1088
  const entry = (0, folderRegistry_1.findEntryByWorkspaceId)((0, folderRegistry_1.loadFolderRegistry)(), workspaceId);
1024
1089
  if (!entry)
1025
1090
  return null;
1091
+ // wmr-04 — same gate as the work-graph pointer on this lane: whether
1092
+ // THIS bridge run actually registered the MCP server. Fetch failures
1093
+ // resolve null and leave the standing instruction in place (PRD D2).
1094
+ const memoryAvailable = mcp.action !== 'claude-not-found';
1095
+ const memoryBrief = memoryAvailable ? await (0, memoryBrief_1.fetchMemoryBrief)(workspaceId) : null;
1026
1096
  return (0, localContextWriter_1.writeLocalContext)({
1027
1097
  projectRoot: entry.path,
1028
1098
  content,
@@ -1034,6 +1104,8 @@ exports.bridgeCommand = new commander_1.Command('bridge')
1034
1104
  // for "are the tools this pointer names actually registered" —
1035
1105
  // pointing at tools that do not exist is worse than silence.
1036
1106
  workGraphAvailable: mcp.action !== 'claude-not-found',
1107
+ memoryAvailable,
1108
+ memoryBrief,
1037
1109
  }, realLocalContextWriterDeps());
1038
1110
  },
1039
1111
  // lws-06 — best-effort `claude` binary check before spawning an
@@ -1182,9 +1254,17 @@ exports.bridgeCommand = new commander_1.Command('bridge')
1182
1254
  workspaceRoot: entry.path,
1183
1255
  mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
1184
1256
  writeFile: (p, content) => fs.writeFileSync(p, content),
1257
+ // Crew swap: replace the previous crew's members instead of
1258
+ // piling on top of them. The installer decides WHAT may go (only
1259
+ // marker-carrying files in .claude/agents that this bundle does
1260
+ // not include); these three only give it eyes and hands.
1261
+ listDir: (dir) => (fs.existsSync(dir) ? fs.readdirSync(dir) : []),
1262
+ readFile: (p) => fs.readFileSync(p, 'utf-8'),
1263
+ remove: (p) => fs.rmSync(p, { force: true }),
1185
1264
  });
1186
1265
  console.log(chalk_1.default.green(`✓ Installed crew bundle → ${entry.path} (${result.written.length} files` +
1187
1266
  (result.skipped.length ? `, ${result.skipped.length} skipped` : '') +
1267
+ (result.pruned.length ? `, ${result.pruned.length} stale crew member(s) removed` : '') +
1188
1268
  ')'));
1189
1269
  return result;
1190
1270
  },
@@ -0,0 +1,9 @@
1
+ /**
2
+ * wha-08 — distinguishing a real API key from the old habit of pasting a
3
+ * bare Solana wallet public key into --api-key (docs/CONTEXT-identity.md:
4
+ * "an API key is always a secret credential, never a public key").
5
+ */
6
+ export declare function isRealApiKey(value: string): boolean;
7
+ export declare function looksLikeWalletPubkey(value: string): boolean;
8
+ /** Short, safe-to-print reference to a secret — never the whole key. */
9
+ export declare function shortenKey(value: string): string;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ /**
3
+ * wha-08 — distinguishing a real API key from the old habit of pasting a
4
+ * bare Solana wallet public key into --api-key (docs/CONTEXT-identity.md:
5
+ * "an API key is always a secret credential, never a public key").
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.isRealApiKey = isRealApiKey;
9
+ exports.looksLikeWalletPubkey = looksLikeWalletPubkey;
10
+ exports.shortenKey = shortenKey;
11
+ // Matches ApiKeyService#generateKey in backend/src/services/api-key.service.ts.
12
+ const API_KEY_PREFIX = 'wm_sk_';
13
+ // Solana public keys are base58 (no 0, O, I, l), typically 32-44 chars.
14
+ const BASE58_PUBKEY_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
15
+ function isRealApiKey(value) {
16
+ return typeof value === 'string' && value.startsWith(API_KEY_PREFIX) && value.length > API_KEY_PREFIX.length;
17
+ }
18
+ function looksLikeWalletPubkey(value) {
19
+ return typeof value === 'string' && !value.startsWith(API_KEY_PREFIX) && BASE58_PUBKEY_RE.test(value);
20
+ }
21
+ /** Short, safe-to-print reference to a secret — never the whole key. */
22
+ function shortenKey(value) {
23
+ if (!value)
24
+ return value;
25
+ return value.length <= 14 ? value : `${value.slice(0, 12)}…`;
26
+ }
@@ -13,9 +13,15 @@ class ApiClient {
13
13
  }
14
14
  async request(method, path, body) {
15
15
  const url = `${this.apiUrl}${path}`;
16
+ // wha-08 (docs/CONTEXT-identity.md, docs/prd/retire-wallet-header-auth.md):
17
+ // the interactive CLI authenticates with an API key, sent as X-API-Key.
18
+ // X-Wallet-Pubkey (the retired header) is never sent by this client —
19
+ // requireConfig() already refuses to load a config whose "apiKey" is
20
+ // actually a bare wallet public key, so `this.apiKey` here is always a
21
+ // real wm_sk_ credential.
16
22
  const headers = {
17
23
  'Content-Type': 'application/json',
18
- 'X-Wallet-Pubkey': this.apiKey,
24
+ 'X-API-Key': this.apiKey,
19
25
  };
20
26
  const res = await fetch(url, {
21
27
  method,
@@ -4,5 +4,8 @@ export interface CliConfig {
4
4
  }
5
5
  export declare function loadConfig(): CliConfig | null;
6
6
  export declare function saveConfig(config: CliConfig): void;
7
+ /** wha-08: deletes the local config file — used by `auth logout`. Silent no-op
8
+ * if it's already gone (e.g. a previous logout, or never logged in). */
9
+ export declare function clearConfig(): void;
7
10
  export declare function requireConfig(): CliConfig;
8
11
  export declare function getApiUrl(): string;
@@ -35,14 +35,17 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.loadConfig = loadConfig;
37
37
  exports.saveConfig = saveConfig;
38
+ exports.clearConfig = clearConfig;
38
39
  exports.requireConfig = requireConfig;
39
40
  exports.getApiUrl = getApiUrl;
40
41
  const fs = __importStar(require("fs"));
41
42
  const path = __importStar(require("path"));
42
43
  const os = __importStar(require("os"));
44
+ const apiKeyFormat_1 = require("./apiKeyFormat");
43
45
  const CONFIG_DIR = path.join(os.homedir(), '.wen-market');
44
46
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
45
- const DEFAULT_API_URL = 'http://localhost:3002/api/v1';
47
+ // Published CLI: default to production. Local development sets MARKETPLACE_API_URL.
48
+ const DEFAULT_API_URL = 'https://api.wenrwa.com/api/v1';
46
49
  function loadConfig() {
47
50
  try {
48
51
  const raw = fs.readFileSync(CONFIG_FILE, 'utf-8');
@@ -56,11 +59,34 @@ function saveConfig(config) {
56
59
  fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
57
60
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
58
61
  }
62
+ /** wha-08: deletes the local config file — used by `auth logout`. Silent no-op
63
+ * if it's already gone (e.g. a previous logout, or never logged in). */
64
+ function clearConfig() {
65
+ try {
66
+ fs.unlinkSync(CONFIG_FILE);
67
+ }
68
+ catch {
69
+ // already gone — nothing to do
70
+ }
71
+ }
59
72
  function requireConfig() {
60
73
  const config = loadConfig();
61
74
  if (!config) {
62
- console.error('Not logged in. Run: wen-market auth login --api-key <key>');
75
+ console.error('Not logged in. Run: wen-market auth login');
76
+ process.exit(1);
77
+ return config;
78
+ }
79
+ // wha-08 (docs/CONTEXT-identity.md): the interactive CLI used to store a
80
+ // bare wallet public key as "apiKey" and send it as X-Wallet-Pubkey (the
81
+ // retired header — public keys are public, so that proved nothing). An
82
+ // existing config left over from that era must not be silently reused —
83
+ // fail fast with guidance rather than let the client send a header that
84
+ // no longer authenticates anything.
85
+ if (!(0, apiKeyFormat_1.isRealApiKey)(config.apiKey)) {
86
+ console.error('Your saved credential looks like a wallet address, not an API key.');
87
+ console.error('Run: wen-market auth login');
63
88
  process.exit(1);
89
+ return config;
64
90
  }
65
91
  return config;
66
92
  }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * wha-08 — best-effort "open the approval URL in a browser" for
3
+ * `wen-market auth login`. The code + URL are always printed regardless of
4
+ * whether this works (over SSH there is no browser at all), so any failure
5
+ * here is swallowed — never surfaced as an error.
6
+ */
7
+ import { SpawnOptions } from 'child_process';
8
+ export type SpawnedChild = {
9
+ unref(): void;
10
+ on(event: 'error', cb: (err: Error) => void): void;
11
+ };
12
+ export type SpawnFn = (command: string, args: string[], options?: SpawnOptions) => SpawnedChild;
13
+ export declare function openBrowser(url: string, opts?: {
14
+ platform?: NodeJS.Platform;
15
+ spawnFn?: SpawnFn;
16
+ }): void;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openBrowser = openBrowser;
4
+ /**
5
+ * wha-08 — best-effort "open the approval URL in a browser" for
6
+ * `wen-market auth login`. The code + URL are always printed regardless of
7
+ * whether this works (over SSH there is no browser at all), so any failure
8
+ * here is swallowed — never surfaced as an error.
9
+ */
10
+ const child_process_1 = require("child_process");
11
+ function commandFor(platform, url) {
12
+ if (platform === 'darwin')
13
+ return { command: 'open', args: [url] };
14
+ if (platform === 'win32')
15
+ return { command: 'cmd', args: ['/c', 'start', '""', url] };
16
+ return { command: 'xdg-open', args: [url] };
17
+ }
18
+ function openBrowser(url, opts = {}) {
19
+ const platform = opts.platform ?? process.platform;
20
+ const spawnFn = opts.spawnFn ?? child_process_1.spawn;
21
+ try {
22
+ const { command, args } = commandFor(platform, url);
23
+ const child = spawnFn(command, args, { stdio: 'ignore', detached: true });
24
+ child.on('error', () => { });
25
+ child.unref();
26
+ }
27
+ catch {
28
+ // best-effort — nothing to do, the user still has the printed URL
29
+ }
30
+ }
@@ -0,0 +1,9 @@
1
+ export interface ConfirmStreams {
2
+ input: NodeJS.ReadableStream;
3
+ output: NodeJS.WritableStream;
4
+ }
5
+ export declare function isInteractive(stdin?: Pick<NodeJS.ReadStream, 'isTTY'>, stdout?: Pick<NodeJS.WriteStream, 'isTTY'>): boolean;
6
+ /** Asks `question`, returns `defaultValue` on a bare Enter. Only "y"/"yes"
7
+ * (case-insensitive) count as yes — everything else, including garbage
8
+ * input, is a no. */
9
+ export declare function confirm(question: string, defaultValue?: boolean, streams?: ConfirmStreams): Promise<boolean>;
@@ -0,0 +1,63 @@
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
+ exports.isInteractive = isInteractive;
37
+ exports.confirm = confirm;
38
+ /**
39
+ * wha-08 — minimal y/N confirmation prompt for "Already signed in as X.
40
+ * Sign in again? (y/N)". Streams are injectable so this is testable without
41
+ * a real TTY.
42
+ */
43
+ const readline = __importStar(require("readline"));
44
+ function isInteractive(stdin = process.stdin, stdout = process.stdout) {
45
+ return Boolean(stdin.isTTY && stdout.isTTY);
46
+ }
47
+ /** Asks `question`, returns `defaultValue` on a bare Enter. Only "y"/"yes"
48
+ * (case-insensitive) count as yes — everything else, including garbage
49
+ * input, is a no. */
50
+ function confirm(question, defaultValue = false, streams = { input: process.stdin, output: process.stdout }) {
51
+ const rl = readline.createInterface({ input: streams.input, output: streams.output });
52
+ return new Promise((resolve) => {
53
+ rl.question(question, (answer) => {
54
+ rl.close();
55
+ const trimmed = answer.trim().toLowerCase();
56
+ if (!trimmed) {
57
+ resolve(defaultValue);
58
+ return;
59
+ }
60
+ resolve(trimmed === 'y' || trimmed === 'yes');
61
+ });
62
+ });
63
+ }