1688-cli 0.1.41 → 0.1.42

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 (83) hide show
  1. package/AGENTS.md +112 -318
  2. package/ARCHITECTURE.md +106 -0
  3. package/README.md +100 -10
  4. package/dist/cli.js +98 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/cart-list.js +2 -1
  7. package/dist/commands/cart-list.js.map +1 -1
  8. package/dist/commands/compare.js +107 -0
  9. package/dist/commands/compare.js.map +1 -0
  10. package/dist/commands/doctor.js +19 -5
  11. package/dist/commands/doctor.js.map +1 -1
  12. package/dist/commands/offer.js +7 -5
  13. package/dist/commands/offer.js.map +1 -1
  14. package/dist/commands/order-list.js +4 -2
  15. package/dist/commands/order-list.js.map +1 -1
  16. package/dist/commands/order-logistics.js +4 -2
  17. package/dist/commands/order-logistics.js.map +1 -1
  18. package/dist/commands/research.js +142 -0
  19. package/dist/commands/research.js.map +1 -0
  20. package/dist/commands/search.js +59 -18
  21. package/dist/commands/search.js.map +1 -1
  22. package/dist/commands/seller-messages.js +7 -4
  23. package/dist/commands/seller-messages.js.map +1 -1
  24. package/dist/commands/sourcing-utils.js +438 -0
  25. package/dist/commands/sourcing-utils.js.map +1 -0
  26. package/dist/commands/supplier-inspect.js +559 -0
  27. package/dist/commands/supplier-inspect.js.map +1 -0
  28. package/dist/commands/supplier-search.js +522 -0
  29. package/dist/commands/supplier-search.js.map +1 -0
  30. package/dist/daemon/protocol.js +2 -1
  31. package/dist/daemon/protocol.js.map +1 -1
  32. package/dist/session/dispatch.js +2 -0
  33. package/dist/session/dispatch.js.map +1 -1
  34. package/dist/session/im-ws.js +8 -5
  35. package/dist/session/im-ws.js.map +1 -1
  36. package/dist/session/paths.js +17 -4
  37. package/dist/session/paths.js.map +1 -1
  38. package/dist/session/search-mtop.js +53 -0
  39. package/dist/session/search-mtop.js.map +1 -1
  40. package/dist/session/supplier-search.js +403 -0
  41. package/dist/session/supplier-search.js.map +1 -0
  42. package/dist/util/encoding.js +8 -0
  43. package/dist/util/encoding.js.map +1 -0
  44. package/dist/util/temp.js +6 -0
  45. package/dist/util/temp.js.map +1 -0
  46. package/docs/AGENT_MAPS_PLAN.md +171 -0
  47. package/docs/AGENT_WORKING_PRINCIPLES.md +143 -0
  48. package/docs/COMMANDS.md +199 -0
  49. package/docs/FEATURES.md +45 -0
  50. package/docs/JSON_CONTRACTS.md +390 -0
  51. package/docs/QUALITY_SCORE.md +60 -0
  52. package/docs/README.md +35 -0
  53. package/docs/RELIABILITY.md +61 -0
  54. package/docs/SAFETY.md +100 -0
  55. package/docs/WORKFLOW.md +82 -0
  56. package/docs/exec-plans/README.md +9 -0
  57. package/docs/exec-plans/active/README.md +4 -0
  58. package/docs/exec-plans/completed/2026-05-28-sourcing-research-v1.md +125 -0
  59. package/docs/exec-plans/completed/2026-05-31-supplier-inspect-v1.md +113 -0
  60. package/docs/exec-plans/completed/2026-06-04-supplier-search-v1.md +81 -0
  61. package/docs/exec-plans/completed/2026-06-07-windows-cli-compatibility.md +138 -0
  62. package/docs/exec-plans/completed/README.md +4 -0
  63. package/docs/exec-plans/tech-debt-tracker.md +5 -0
  64. package/docs/generated/command-index.md +54 -0
  65. package/docs/generated/json-shapes.md +111 -0
  66. package/docs/generated/module-map.md +13 -0
  67. package/docs/generated/test-index.md +34 -0
  68. package/docs/playbooks/add-command.md +15 -0
  69. package/docs/playbooks/add-mtop-capture.md +13 -0
  70. package/docs/playbooks/change-json-output.md +11 -0
  71. package/docs/playbooks/debug-risk-control.md +12 -0
  72. package/docs/playbooks/update-cli-release.md +11 -0
  73. package/docs/specs/checkout-and-orders.md +30 -0
  74. package/docs/specs/seller-im.md +28 -0
  75. package/docs/specs/sourcing-research.md +186 -0
  76. package/docs/specs/supplier-inspect.md +144 -0
  77. package/docs/specs/supplier-search.md +179 -0
  78. package/docs/specs/windows-cli-compatibility.md +123 -0
  79. package/package.json +12 -2
  80. package/scripts/check_agent_map.mjs +86 -0
  81. package/scripts/fix_bin_mode.mjs +18 -0
  82. package/scripts/generate_agent_context.mjs +253 -0
  83. package/scripts/postinstall.mjs +12 -4
@@ -0,0 +1,179 @@
1
+ # Supplier Search And Research
2
+
3
+ This spec defines supplier discovery that starts from 1688's company search,
4
+ not product-offer aggregation.
5
+
6
+ ## Goal
7
+
8
+ Help a buyer or agent answer:
9
+
10
+ - Which suppliers match this category keyword?
11
+ - Which matching suppliers expose factory/trust/service signals?
12
+ - Which suppliers deserve deeper `supplier inspect` enrichment?
13
+ - Which suppliers should be contacted or compared after product discovery?
14
+
15
+ ## Source Boundary
16
+
17
+ `supplier search` and `supplier research` must use 1688 company search. The
18
+ known durable business endpoint from live probing is:
19
+
20
+ ```text
21
+ search.1688.com/service/companySearchBusinessService
22
+ ```
23
+
24
+ The page entry URL is:
25
+
26
+ ```text
27
+ https://s.1688.com/company/company_search.htm?keywords=<GBK-percent-keyword>
28
+ ```
29
+
30
+ Important encoding rule: `s.1688.com` expects GBK percent-encoded keywords.
31
+ UTF-8 percent-encoding can search for mojibake and return zero or irrelevant
32
+ results.
33
+
34
+ Do not implement supplier discovery by running offer search and grouping
35
+ offers by supplier. That is a different signal and can hide suppliers that are
36
+ available in company search.
37
+
38
+ ## Commands
39
+
40
+ ### `supplier search`
41
+
42
+ ```bash
43
+ 1688 supplier search <keyword...> \
44
+ --max 20 \
45
+ --factory-only \
46
+ --province 广东 \
47
+ --city 深圳 \
48
+ --min-years 3 \
49
+ --min-repeat-rate 0.4 \
50
+ --min-response-rate 0.6 \
51
+ --enrich 0
52
+ ```
53
+
54
+ Default behavior is supplier discovery only. `--enrich` is optional and
55
+ defaults to `0`.
56
+
57
+ ### `supplier research`
58
+
59
+ ```bash
60
+ 1688 supplier research <keyword...> \
61
+ --max 20 \
62
+ --factory-only \
63
+ --enrich top:10 \
64
+ --jsonl
65
+ ```
66
+
67
+ `supplier research` uses the same company-search source and scoring, but
68
+ defaults to `--enrich top:10`. Enrichment calls `supplier inspect` with the
69
+ company-search `memberId` when present.
70
+
71
+ Supported export modes:
72
+
73
+ - default: human table
74
+ - JSON: automatic when stdout is piped or `--json` is used
75
+ - `--jsonl`: one supplier item per line
76
+ - `--csv`: comma-separated table
77
+ - `--output <file>`: write JSONL/CSV to a file
78
+
79
+ ## Data Model
80
+
81
+ Each item records source keyword/rank, normalized company-search supplier
82
+ signals, score, and optional inspect enrichment:
83
+
84
+ ```ts
85
+ {
86
+ sourceKeyword: string,
87
+ sourceRank: number,
88
+ globalRank: number,
89
+ supplier: {
90
+ companyName: string,
91
+ loginId: string | null,
92
+ memberId: string | null,
93
+ enterpriseId: string | null,
94
+ realUserId: string | null,
95
+ companyId: string | null,
96
+ shopUrl: string | null,
97
+ factoryCardUrl: string | null,
98
+ location: { province: string | null, city: string | null, address: string | null },
99
+ productionService: string | null,
100
+ tp: { serviceYears: number | null, memberLevel: string | null },
101
+ factory: {
102
+ isFactory: boolean,
103
+ factoryTag: string | null,
104
+ factoryLevel: string | null,
105
+ superFactory: boolean,
106
+ businessInspection: boolean,
107
+ factoryInspection: boolean,
108
+ },
109
+ service: {
110
+ compositeScore: number | null,
111
+ wwResponseRate: number | null,
112
+ repeatRate: number | null,
113
+ },
114
+ demand: {
115
+ payOrderCount3m: number | null,
116
+ payAmount3m: number | null,
117
+ fuzzyPayAmount3m: string | null,
118
+ saleQuantity3m: number | null,
119
+ },
120
+ tags: string[],
121
+ offersPreview: SupplierOfferPreview[],
122
+ },
123
+ score: number,
124
+ scoreBreakdown: Array<{ name: string, points: number, reason: string }>,
125
+ inspect?: SupplierInspectResult,
126
+ error?: { code: string, message: string },
127
+ }
128
+ ```
129
+
130
+ The top-level result includes:
131
+
132
+ ```ts
133
+ source: {
134
+ kind: "company-search",
135
+ endpoint: "companySearchBusinessService",
136
+ offerAggregation: false,
137
+ }
138
+ ```
139
+
140
+ ## Score V1
141
+
142
+ The supplier score is a ranking aid, not a truth claim.
143
+
144
+ - Company-search demand: up to 25 points from 3-month pay order count.
145
+ - Supplier tenure: up to 15 points from service years.
146
+ - Factory/trust: up to 20 points from factory/super-factory/inspection flags.
147
+ - Service rates: up to 15 points from repeat and Wangwang response rates.
148
+ - Composite score: up to 10 points.
149
+ - Offer preview depth: up to 10 points from company-search previews.
150
+
151
+ ## Failure Semantics
152
+
153
+ - Run-level failure: login expired, risk control, browser/network failure that
154
+ prevents company search from loading.
155
+ - Empty result: company search loads but returns no supplier payload.
156
+ - Item-level enrichment failure: `supplier inspect` fails for one supplier;
157
+ keep the supplier item and attach `error`.
158
+
159
+ If a command exits with risk-control code `4`, retry once with `--headed` and
160
+ solve the slider manually.
161
+
162
+ ## V1 Boundaries
163
+
164
+ Live probing on 2026-06-04 showed the company search page emits
165
+ `companySearchBusinessService` with `companyWithOfferLists`. A typical first
166
+ page async response used `startIndex=6&asyncCount=14`; this likely means some
167
+ top-page suppliers may be server-rendered before the async service response.
168
+ V1 uses the stable browser-emitted business response and keeps the largest
169
+ captured company-search payload. A later V2 can add HTML/DOM extraction for
170
+ server-rendered supplier cards if we need exact 20-per-page completeness.
171
+
172
+ ## Verification
173
+
174
+ - Unit tests cover GBK company-search URL construction.
175
+ - Unit tests cover `companySearchBusinessService` parsing and offer previews.
176
+ - Unit tests cover capture `keep: "largest"` behavior.
177
+ - Unit tests cover enrich option parsing and CSV escaping.
178
+ - `pnpm agent-context` refreshes generated command and JSON-shape indexes.
179
+ - `pnpm agent-verify` is the default gate.
@@ -0,0 +1,123 @@
1
+ # Windows CLI Compatibility
2
+
3
+ This spec defines the compatibility baseline required before `1688-cli` can
4
+ claim normal Windows command-line support.
5
+
6
+ ## Goal
7
+
8
+ Make the installed CLI and local development workflow work from Windows
9
+ PowerShell and cmd.exe for read-only and daemon-backed commands.
10
+
11
+ ## Scope
12
+
13
+ - npm package install and build scripts must not depend on Unix shell commands.
14
+ - The daemon IPC path must work on Windows named pipes and avoid collisions
15
+ between different `BB1688_HOME` roots.
16
+ - Runtime paths, temporary files, and diagnostics must use Node path helpers
17
+ instead of hard-coded Unix paths where they are part of normal command code.
18
+ - `doctor` fix hints must be executable or meaningful on Windows.
19
+ - README and command docs must show Windows alternatives where Unix examples
20
+ use `jq`, shell assignment, `/tmp`, or `~/.1688`.
21
+ - Deterministic tests must cover the Windows-specific path and hint logic.
22
+ - CI/package verification must include a Windows-compatible build and smoke
23
+ path, or document the exact manual Windows checks when CI is not available
24
+ in the current environment.
25
+
26
+ ## Non-Goals
27
+
28
+ - Do not bypass 1688 login, slider verification, or risk control.
29
+ - Do not automate Windows UI interaction beyond Playwright's existing browser
30
+ launch behavior.
31
+ - Do not guarantee live 1688 network/search success in CI; CI checks should be
32
+ deterministic and use `doctor --no-launch` unless a real account/session is
33
+ explicitly supplied.
34
+ - Do not change public JSON contracts except by additive diagnostic fields.
35
+ - Do not add a new installer, native binary, or Windows service wrapper.
36
+
37
+ ## Behavior Contract
38
+
39
+ ### Build And Install
40
+
41
+ `pnpm build` must run on Windows, macOS, and Linux. Any executable-bit fix must
42
+ be implemented in Node and become a no-op on Windows.
43
+
44
+ `scripts/postinstall.mjs` must:
45
+
46
+ - locate the daemon pid file under `BB1688_HOME` when set
47
+ - detect Windows Chrome install paths
48
+ - invoke `npx.cmd` on Windows and `npx` elsewhere
49
+ - print retry commands that are valid for the current platform
50
+
51
+ ### Daemon IPC
52
+
53
+ Unix-like platforms continue to use `<BB1688_HOME>/daemon.sock`.
54
+
55
+ Windows must use a named pipe:
56
+
57
+ ```text
58
+ \\.\pipe\1688-cli-daemon-<stable-root-hash>
59
+ ```
60
+
61
+ The hash must be stable for a given `BB1688_HOME`/default root and different
62
+ for different roots so tests, profiles, and concurrent users do not collide.
63
+
64
+ ### Diagnostics
65
+
66
+ `1688 doctor` must emit platform-appropriate fix hints:
67
+
68
+ - Unix-like stale lock: `rm -rf "..."`
69
+ - Windows stale lock: `Remove-Item -Recurse -Force "..."`
70
+ - Unix-like writable-directory issue: `chmod u+w "..."`
71
+ - Windows writable-directory issue: explain to grant write permission or choose
72
+ another `BB1688_HOME`
73
+
74
+ The daemon protocol documentation and README must not describe the daemon as
75
+ Unix-only.
76
+
77
+ ### Documentation
78
+
79
+ README and command docs must include:
80
+
81
+ - PowerShell examples using built-in `--get`/`--pick` instead of requiring `jq`
82
+ - Windows output paths such as `$env:TEMP\suppliers.csv`
83
+ - Windows local state paths using `%USERPROFILE%\.1688` or `$env:USERPROFILE`
84
+ - named pipe note for daemon IPC on Windows
85
+
86
+ ## Acceptance Criteria
87
+
88
+ - `pnpm build` succeeds without requiring `chmod` from the shell.
89
+ - Unit tests cover Windows named pipe generation and platform-specific doctor
90
+ hints.
91
+ - `pnpm test:unit` passes.
92
+ - `pnpm agent-verify` passes.
93
+ - `npm pack --dry-run` succeeds.
94
+ - README and `docs/COMMANDS.md` no longer present Unix-only examples as the
95
+ only way to use JSON, output files, or local paths.
96
+ - Manual Windows smoke checklist is documented:
97
+ - `npm i -g 1688-cli`
98
+ - `1688 --version`
99
+ - `1688 doctor --no-launch --json`
100
+ - `1688 daemon start`
101
+ - `1688 daemon status --json`
102
+ - `1688 daemon stop`
103
+ - `1688 search 雨伞 --max 1 --json`
104
+ - `1688 supplier search 键盘 --max 1 --json`
105
+
106
+ ## Verification Signals
107
+
108
+ - Focused tests:
109
+ - `tests/paths.test.ts`
110
+ - `tests/doctor.test.ts` or existing doctor tests
111
+ - Package checks:
112
+ - `pnpm build`
113
+ - `pnpm test:unit`
114
+ - `pnpm agent-verify`
115
+ - `npm pack --dry-run`
116
+ - Manual Windows smoke checks listed above when a Windows machine/session is
117
+ available.
118
+
119
+ ## Open Questions
120
+
121
+ - Whether to add GitHub Actions `windows-latest` is a repository operations
122
+ decision. The code/docs work should make that job straightforward, but adding
123
+ CI config is not required unless the repository already uses GitHub Actions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "1688-cli",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "description": "1688.com CLI for humans, Codex, and Claude Code. Sourcing (search / image-search / offer / inquire) and orders (list / detail / logistics / seller chat).",
5
5
  "license": "MIT",
6
6
  "author": "nobodyjack",
@@ -36,7 +36,12 @@
36
36
  "files": [
37
37
  "dist",
38
38
  "scripts/postinstall.mjs",
39
+ "scripts/fix_bin_mode.mjs",
40
+ "scripts/generate_agent_context.mjs",
41
+ "scripts/check_agent_map.mjs",
42
+ "docs",
39
43
  "AGENTS.md",
44
+ "ARCHITECTURE.md",
40
45
  "README.md",
41
46
  "LICENSE",
42
47
  "CHANGELOG.md"
@@ -46,10 +51,15 @@
46
51
  },
47
52
  "scripts": {
48
53
  "dev": "tsx src/cli.ts",
49
- "build": "tsc -p tsconfig.json && chmod +x dist/cli.js",
54
+ "build": "tsc -p tsconfig.json && node scripts/fix_bin_mode.mjs",
50
55
  "test": "vitest run",
56
+ "test:unit": "vitest run --exclude tests/doctor-live.test.ts",
51
57
  "test:watch": "vitest",
52
58
  "typecheck": "tsc --noEmit",
59
+ "agent-context": "node scripts/generate_agent_context.mjs",
60
+ "docs-check": "node scripts/generate_agent_context.mjs --check",
61
+ "agent-map-check": "node scripts/check_agent_map.mjs",
62
+ "agent-verify": "pnpm typecheck && pnpm test:unit && pnpm docs-check && pnpm agent-map-check",
53
63
  "prepublishOnly": "pnpm build",
54
64
  "postinstall": "node scripts/postinstall.mjs"
55
65
  },
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
7
+
8
+ async function read(relPath) {
9
+ return fs.readFile(path.join(root, relPath), 'utf8');
10
+ }
11
+
12
+ async function exists(relPath) {
13
+ try {
14
+ await fs.access(path.join(root, relPath));
15
+ return true;
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ const requiredFiles = [
22
+ 'AGENTS.md',
23
+ 'ARCHITECTURE.md',
24
+ 'docs/AGENT_WORKING_PRINCIPLES.md',
25
+ 'docs/README.md',
26
+ 'docs/WORKFLOW.md',
27
+ 'docs/COMMANDS.md',
28
+ 'docs/JSON_CONTRACTS.md',
29
+ 'docs/SAFETY.md',
30
+ 'docs/RELIABILITY.md',
31
+ 'docs/QUALITY_SCORE.md',
32
+ 'docs/FEATURES.md',
33
+ 'docs/specs/sourcing-research.md',
34
+ 'docs/specs/seller-im.md',
35
+ 'docs/specs/checkout-and-orders.md',
36
+ 'docs/playbooks/add-command.md',
37
+ 'docs/playbooks/change-json-output.md',
38
+ 'docs/playbooks/debug-risk-control.md',
39
+ 'docs/playbooks/add-mtop-capture.md',
40
+ 'docs/playbooks/update-cli-release.md',
41
+ 'docs/generated/command-index.md',
42
+ 'docs/generated/module-map.md',
43
+ 'docs/generated/test-index.md',
44
+ 'docs/generated/json-shapes.md',
45
+ ];
46
+
47
+ const missing = [];
48
+ for (const file of requiredFiles) {
49
+ if (!(await exists(file))) missing.push(file);
50
+ }
51
+
52
+ const failures = [];
53
+ if (missing.length) failures.push(`Missing files: ${missing.join(', ')}`);
54
+
55
+ const agents = await read('AGENTS.md').catch(() => '');
56
+ if (!agents.includes('pnpm agent-verify'))
57
+ failures.push('AGENTS.md must mention pnpm agent-verify.');
58
+ if (!agents.includes('docs/playbooks'))
59
+ failures.push('AGENTS.md must route work to docs/playbooks.');
60
+ if (!agents.includes('docs/AGENT_WORKING_PRINCIPLES.md'))
61
+ failures.push('AGENTS.md must link docs/AGENT_WORKING_PRINCIPLES.md.');
62
+ if (agents.split('\n').length > 220)
63
+ failures.push('AGENTS.md should stay short (<= 220 lines).');
64
+
65
+ const pkg = JSON.parse(await read('package.json'));
66
+ for (const scriptName of ['agent-context', 'docs-check', 'agent-map-check', 'agent-verify']) {
67
+ if (!pkg.scripts?.[scriptName]) failures.push(`package.json missing script: ${scriptName}`);
68
+ }
69
+
70
+ const docsReadme = await read('docs/README.md').catch(() => '');
71
+ for (const needle of [
72
+ 'AGENT_WORKING_PRINCIPLES.md',
73
+ 'COMMANDS.md',
74
+ 'JSON_CONTRACTS.md',
75
+ 'SAFETY.md',
76
+ 'generated/',
77
+ ]) {
78
+ if (!docsReadme.includes(needle)) failures.push(`docs/README.md must link ${needle}.`);
79
+ }
80
+
81
+ if (failures.length) {
82
+ console.error(failures.join('\n'));
83
+ process.exit(1);
84
+ }
85
+
86
+ console.log('Agent map structure looks good.');
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+
5
+ export async function fixBinMode({
6
+ platform = process.platform,
7
+ target = path.join('dist', 'cli.js'),
8
+ } = {}) {
9
+ if (platform === 'win32') {
10
+ return { changed: false, reason: 'windows-noop', target };
11
+ }
12
+ await fs.chmod(target, 0o755);
13
+ return { changed: true, reason: 'chmod-755', target };
14
+ }
15
+
16
+ if (import.meta.url === `file://${process.argv[1]}`) {
17
+ await fixBinMode();
18
+ }
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
7
+ const outDir = path.join(root, 'docs', 'generated');
8
+ const checkOnly = process.argv.includes('--check');
9
+
10
+ const rel = (p) => path.relative(root, p).replaceAll(path.sep, '/');
11
+ const read = (p) => fs.readFile(path.join(root, p), 'utf8');
12
+
13
+ async function exists(p) {
14
+ try {
15
+ await fs.access(path.join(root, p));
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ async function walk(dir, predicate = () => true) {
23
+ const abs = path.join(root, dir);
24
+ const out = [];
25
+ async function visit(current) {
26
+ const entries = await fs.readdir(current, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
29
+ const p = path.join(current, entry.name);
30
+ if (entry.isDirectory()) {
31
+ await visit(p);
32
+ } else if (predicate(p)) {
33
+ out.push(p);
34
+ }
35
+ }
36
+ }
37
+ if (await exists(dir)) await visit(abs);
38
+ return out.sort((a, b) => rel(a).localeCompare(rel(b)));
39
+ }
40
+
41
+ function header(title) {
42
+ return `# ${title}\n\n_Generated by \`scripts/generate_agent_context.mjs\`._\n\n`;
43
+ }
44
+
45
+ function cleanDescription(raw) {
46
+ if (!raw) return '-';
47
+ return raw
48
+ .replace(/[`'"]/g, '')
49
+ .replace(/\s*\+\s*/g, ' ')
50
+ .replace(/\s+/g, ' ')
51
+ .trim()
52
+ .replace(/\|/g, '\\|') || '-';
53
+ }
54
+
55
+ function compactList(items) {
56
+ return items.length ? items.map((x) => `\`${x}\``).join('<br>') : '-';
57
+ }
58
+
59
+ async function commandIndex() {
60
+ const cli = await read('src/cli.ts');
61
+ const re =
62
+ /(?:(?:const\s+([A-Za-z0-9_]+)\s*=\s*)?([A-Za-z0-9_]+)\s*\n\s*\.command\('([^']+)'\))/g;
63
+ const matches = [...cli.matchAll(re)];
64
+ const varToCommand = new Map();
65
+ const rows = [];
66
+
67
+ for (let i = 0; i < matches.length; i++) {
68
+ const m = matches[i];
69
+ const assignedVar = m[1];
70
+ const receiver = m[2];
71
+ const command = m[3];
72
+ const start = m.index ?? 0;
73
+ const end = matches[i + 1]?.index ?? cli.length;
74
+ const block = cli.slice(start, end);
75
+ const parent = receiver === 'program' ? '' : varToCommand.get(receiver) ?? receiver;
76
+ const full = parent ? `${parent} ${command}` : command;
77
+ if (assignedVar) varToCommand.set(assignedVar, full);
78
+
79
+ const descRaw = block.match(/\.description\(([\s\S]*?)\)\s*(?:\.|\n)/)?.[1];
80
+ const args = [...block.matchAll(/\.argument\(\s*(['"`])([^'"`]+)\1/g)].map(
81
+ (x) => x[2],
82
+ );
83
+ const opts = [
84
+ ...block.matchAll(/\.(?:requiredOption|option)\(\s*(['"`])([^'"`]+)\1/g),
85
+ ].map((x) => x[2]);
86
+ const source = block.match(/import\('\.\/commands\/([^']+)\.js'\)/)?.[1];
87
+ rows.push({
88
+ command: full,
89
+ source: source ? `src/commands/${source}.ts` : '-',
90
+ args,
91
+ opts,
92
+ description: cleanDescription(descRaw),
93
+ });
94
+ }
95
+
96
+ let out = header('Command Index');
97
+ out += '| Command | Source | Arguments | Options | Description |\n';
98
+ out += '|---|---|---|---|---|\n';
99
+ for (const row of rows) {
100
+ out += `| \`${row.command}\` | ${row.source === '-' ? '-' : `\`${row.source}\``} | ${compactList(
101
+ row.args,
102
+ )} | ${compactList(row.opts)} | ${row.description} |\n`;
103
+ }
104
+ return out;
105
+ }
106
+
107
+ async function moduleMap() {
108
+ const files = await walk('src', (p) => p.endsWith('.ts'));
109
+ const tests = await walk('tests', (p) => p.endsWith('.test.ts') || p.endsWith('.spec.ts'));
110
+ const dirs = new Map();
111
+ for (const file of files) {
112
+ const r = rel(file);
113
+ const parts = r.split('/');
114
+ const dir = parts.length > 2 ? `${parts[0]}/${parts[1]}` : parts[0];
115
+ const entry = dirs.get(dir) ?? { files: 0, tests: 0, notes: '-' };
116
+ entry.files++;
117
+ dirs.set(dir, entry);
118
+ }
119
+ for (const file of tests) {
120
+ const content = await fs.readFile(file, 'utf8');
121
+ const target = content.match(/from ['"]\.\.\/src\/([^/'"]+)/)?.[1];
122
+ if (target) {
123
+ const key = `src/${target}`;
124
+ const entry = dirs.get(key) ?? { files: 0, tests: 0, notes: '-' };
125
+ entry.tests++;
126
+ dirs.set(key, entry);
127
+ }
128
+ }
129
+
130
+ const notes = {
131
+ 'src/commands': 'command executors and renderers',
132
+ 'src/session': 'browser/session automation helpers',
133
+ 'src/daemon': 'background daemon client/server/protocol',
134
+ 'src/io': 'output, prompts, and errors',
135
+ 'src/auth': 'login/session/cookie helpers',
136
+ 'src/util': 'shared utilities',
137
+ };
138
+
139
+ let out = header('Module Map');
140
+ out += '| Directory | Source Files | Related Tests | Notes |\n';
141
+ out += '|---|---:|---:|---|\n';
142
+ for (const dir of [...dirs.keys()].sort()) {
143
+ const entry = dirs.get(dir);
144
+ out += `| \`${dir}\` | ${entry.files} | ${entry.tests} | ${notes[dir] ?? entry.notes} |\n`;
145
+ }
146
+ return out;
147
+ }
148
+
149
+ async function testIndex() {
150
+ const tests = await walk('tests', (p) => p.endsWith('.test.ts') || p.endsWith('.spec.ts'));
151
+ let out = header('Test Index');
152
+ out += '| Test File | Focus | Risk Notes |\n';
153
+ out += '|---|---|---|\n';
154
+ for (const file of tests) {
155
+ const content = await fs.readFile(file, 'utf8');
156
+ const name = path.basename(file).replace(/\.(test|spec)\.ts$/, '');
157
+ const risks = [];
158
+ if (/live|doctor-live|process\.env|BB1688|1688\s/.test(content + name))
159
+ risks.push('live/session-sensitive');
160
+ if (/playwright|BrowserContext|Page|page-state|risk-control/i.test(content))
161
+ risks.push('browser/page-state');
162
+ if (/fixture|jsonp|mtop|capture/i.test(content)) risks.push('fixture/parser');
163
+ out += `| \`${rel(file)}\` | ${name} | ${risks.join(', ') || '-'} |\n`;
164
+ }
165
+ return out;
166
+ }
167
+
168
+ function extractInterfaces(source, file) {
169
+ const lines = source.split('\n');
170
+ const interfaces = [];
171
+ for (let i = 0; i < lines.length; i++) {
172
+ const m = lines[i].match(/^export interface ([A-Za-z0-9_]+)/);
173
+ if (!m) continue;
174
+ const name = m[1];
175
+ const keep =
176
+ /(Args|Opts|Result|Offer|Message|CartItem|Order|Sku|Price|Attribute|Package)/.test(
177
+ name,
178
+ );
179
+ if (!keep) continue;
180
+
181
+ const body = [];
182
+ let depth = 0;
183
+ for (let j = i; j < lines.length; j++) {
184
+ const line = lines[j];
185
+ if (j > i) body.push(line);
186
+ depth += (line.match(/\{/g) ?? []).length;
187
+ depth -= (line.match(/\}/g) ?? []).length;
188
+ if (j > i && depth <= 0) break;
189
+ }
190
+ const fields = body
191
+ .map((line) => line.trim())
192
+ .filter((line) => /^[A-Za-z0-9_?]+:/.test(line) || /^\/\*\*/.test(line))
193
+ .slice(0, 14)
194
+ .map((line) => line.replace(/\|/g, '\\|'));
195
+ interfaces.push({ name, file, fields });
196
+ }
197
+ return interfaces;
198
+ }
199
+
200
+ async function jsonShapes() {
201
+ const files = await walk('src', (p) => p.endsWith('.ts'));
202
+ const interfaces = [];
203
+ for (const file of files) {
204
+ const source = await fs.readFile(file, 'utf8');
205
+ interfaces.push(...extractInterfaces(source, rel(file)));
206
+ }
207
+
208
+ let out = header('JSON Shapes');
209
+ out +=
210
+ 'This is a heuristic index of exported TypeScript interfaces that are likely to matter for agent-facing JSON.\n\n';
211
+ out += '| Interface | File | Notable Fields |\n';
212
+ out += '|---|---|---|\n';
213
+ for (const it of interfaces.sort((a, b) => a.file.localeCompare(b.file) || a.name.localeCompare(b.name))) {
214
+ out += `| \`${it.name}\` | \`${it.file}\` | ${it.fields.length ? it.fields.map((x) => `\`${x}\``).join('<br>') : '-'} |\n`;
215
+ }
216
+ return out;
217
+ }
218
+
219
+ const outputs = new Map([
220
+ ['command-index.md', await commandIndex()],
221
+ ['module-map.md', await moduleMap()],
222
+ ['test-index.md', await testIndex()],
223
+ ['json-shapes.md', await jsonShapes()],
224
+ ]);
225
+
226
+ if (checkOnly) {
227
+ const mismatches = [];
228
+ for (const [name, content] of outputs) {
229
+ const target = path.join(outDir, name);
230
+ let current = '';
231
+ try {
232
+ current = await fs.readFile(target, 'utf8');
233
+ } catch {
234
+ mismatches.push(name);
235
+ continue;
236
+ }
237
+ if (current !== content) mismatches.push(name);
238
+ }
239
+ if (mismatches.length) {
240
+ console.error(
241
+ `Generated agent context is stale: ${mismatches.join(', ')}. Run pnpm agent-context.`,
242
+ );
243
+ process.exit(1);
244
+ }
245
+ console.log('Generated agent context is fresh.');
246
+ } else {
247
+ await fs.mkdir(outDir, { recursive: true });
248
+ for (const [name, content] of outputs) {
249
+ await fs.writeFile(path.join(outDir, name), content);
250
+ }
251
+ console.log(`Generated agent context files in ${rel(outDir)}`);
252
+ }
253
+