tor-to-proposal 2.0.0

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 (44) hide show
  1. package/.claude-plugin/marketplace.json +13 -0
  2. package/.claude-plugin/plugin.json +9 -0
  3. package/LICENSE +21 -0
  4. package/README.md +95 -0
  5. package/SKILL.md +189 -0
  6. package/assets/templates.md +74 -0
  7. package/bin/audit.mjs +60 -0
  8. package/bin/bid-pack.mjs +348 -0
  9. package/bin/cover-letter.mjs +67 -0
  10. package/bin/cv-gap.mjs +90 -0
  11. package/bin/cv-tailor.mjs +261 -0
  12. package/bin/dossier.mjs +32 -0
  13. package/bin/extract.mjs +148 -0
  14. package/bin/financial-proposal.mjs +114 -0
  15. package/bin/lib.mjs +109 -0
  16. package/bin/market-rates.mjs +54 -0
  17. package/bin/package.mjs +234 -0
  18. package/bin/pdf-extract.mjs +135 -0
  19. package/bin/pricing-model.mjs +73 -0
  20. package/bin/profile.mjs +201 -0
  21. package/bin/render.mjs +199 -0
  22. package/bin/simulator.mjs +67 -0
  23. package/bin/template-filler.mjs +160 -0
  24. package/bin/tor-to-proposal.mjs +63 -0
  25. package/commands/tor-audit.md +10 -0
  26. package/commands/tor-bid.md +18 -0
  27. package/commands/tor-cv.md +10 -0
  28. package/commands/tor-pack.md +11 -0
  29. package/commands/tor-price.md +11 -0
  30. package/commands/tor-screen.md +11 -0
  31. package/docs/PROMPT-GUIDE.md +245 -0
  32. package/docs/TECHNICAL-GUIDE.md +211 -0
  33. package/docs/USER-GUIDE.md +228 -0
  34. package/library/SCHEMA.md +45 -0
  35. package/library/bids/.gitkeep +0 -0
  36. package/mcp/server.mjs +358 -0
  37. package/package.json +57 -0
  38. package/references/packs/eu-prag.md +29 -0
  39. package/references/packs/undp.md +30 -0
  40. package/references/packs/usaid.md +29 -0
  41. package/references/packs/worldbank.md +26 -0
  42. package/references/pricing-standards.md +52 -0
  43. package/references/render-specs.md +60 -0
  44. package/references/review-rules.md +38 -0
@@ -0,0 +1,228 @@
1
+ # tor-to-proposal — Plain-Language User Guide
2
+
3
+ **For consultants who are not programmers.** No jargon assumed. If you can use
4
+ Gmail and Microsoft Word, you can use this.
5
+
6
+ ---
7
+
8
+ ## 1. What is this thing?
9
+
10
+ You download a document called a **ToR** (Terms of Reference — the UN/NGO's
11
+ description of a consulting job). Together with your **CV**, this tool turns it
12
+ into a **complete bid pack** — the documents you actually submit:
13
+
14
+ - a customized **cover letter** (one page, checked),
15
+ - a **tailored CV** (your real CV, re-ordered for this assignment — never
16
+ improved with facts you don't have),
17
+ - a **technical response** written one section per scoring criterion,
18
+ - a **financial proposal** (with an Excel version where every formula shows),
19
+ - a **submission checklist** and a calendar reminder for the deadline.
20
+
21
+ All of it arrives in one folder, checked, ready to submit.
22
+
23
+ **What it will NEVER do:**
24
+ - Invent experience you don't have. (Your tailored CV is verified line-by-line
25
+ against your real CV.)
26
+ - Make up a price for you.
27
+ - Write "world-class consultancy" nonsense. (It actually bans those words.)
28
+
29
+ > **Why so strict?** In UN/NGO bidding, one made-up fact = disqualification,
30
+ > and sometimes blacklisting from future work. This tool would rather show you
31
+ > a yellow `[FILL]` box saying "put your real number here" than fabricate
32
+ > anything.
33
+
34
+ ---
35
+
36
+ ## 2. What you need before starting
37
+
38
+ Gather these into one folder on your computer (e.g. `Documents/bids/UNDP-2026-042/`):
39
+
40
+ | # | Item | Where you get it |
41
+ | --- | --- | --- |
42
+ | 1 | The **ToR document** (PDF or Word) | The UN/NGO procurement website (UNGM, agency site) |
43
+ | 2 | Your **master CV** as a text or Word file | Your files — the version with your real numbers (years, countries, budgets) |
44
+ | 3 | Your **money numbers** | Your minimum acceptable yearly income, your monthly costs (for pricing) |
45
+ | 4 | Your **calendar** | So you can honestly answer "can I start when they need me?" |
46
+
47
+ ---
48
+
49
+ ## 3. How you use it (one conversation)
50
+
51
+ You talk to Claude (or ChatGPT, Codex, or similar) in normal English. The AI
52
+ runs everything and explains the results. You never see a command line unless
53
+ you want to.
54
+
55
+ **One-time setup (5 minutes):** install once with a single command and connect
56
+ your AI assistant (exact keystrokes in §4). Then say things like:
57
+
58
+ > *"Run a bid pack on my folder UNDP-2026-042."*
59
+
60
+ The assistant will:
61
+
62
+ 1. **Read the ToR and screen it** — one page: title, deadline, effort, how
63
+ they'll score you, red flags. **You decide: go or no-go.** No-go saves you
64
+ a wasted week; nothing is written yet.
65
+ 2. **Ask you every question in ONE message** — the consolidated
66
+ questionnaire: your rate (or a one-word "yes" to your saved rates), your
67
+ availability date, missing facts. This is the only "paperwork" you do.
68
+ 3. **Draft all four documents** — and check each one mechanically: no
69
+ superlatives in the letter, every CV line provable from your master CV,
70
+ every scored criterion covered.
71
+ 4. **Package and verify** — the final PDFs are read back by the tool and
72
+ audited again (the check applies to what the client will read, not the
73
+ draft). You get a folder with the finished documents, a submission
74
+ checklist, and a calendar file for the deadline.
75
+
76
+ **The golden rule:** when you see `[FILL]` in anything, that is a question
77
+ addressed to YOU. Answer it with real facts, or leave it and fix before
78
+ submitting. Never let anyone "just make something up" for a `[FILL]`.
79
+
80
+ ---
81
+
82
+ ## 4. One-time installation
83
+
84
+ Ask a technical friend if unsure — it is two short steps. **Mac/Linux
85
+ Terminal** (Windows: PowerShell, after installing Node.js from nodejs.org).
86
+
87
+ **Step 1 — Install (one command):**
88
+
89
+ ```bash
90
+ npx tor-to-proposal init
91
+ ```
92
+
93
+ This installs the tool and asks you a few questions (name, credentials, your
94
+ rate numbers, where your master CV lives). Answers are saved on YOUR computer
95
+ only. Next time you prepare a bid, the assistant offers your saved numbers for
96
+ a one-word confirmation instead of asking again.
97
+
98
+ **Step 2 — Connect it to your AI assistant (pick yours):**
99
+
100
+ - **Claude Desktop** (the chat app): open Settings → Developer → Edit Config,
101
+ and add to the JSON file:
102
+ ```json
103
+ {
104
+ "mcpServers": {
105
+ "tor-to-proposal": {
106
+ "command": "npx",
107
+ "args": ["-y", "tor-to-proposal", "mcp-server"]
108
+ }
109
+ }
110
+ }
111
+ ```
112
+ (If you cloned the repo instead of using npx, point the config at
113
+ `/FULL-PATH-TO/tor-to-proposal/mcp/server.mjs` — find the path by typing
114
+ `pwd` in the tool's folder. Restart Claude Desktop.)
115
+
116
+ - **Claude Code / Codex CLI** (terminal AI): run
117
+ ```bash
118
+ claude mcp add tor-to-proposal -- npx -y tor-to-proposal mcp-server
119
+ ```
120
+ or for Codex, add the same server to `~/.codex/config.toml` (exact lines in
121
+ the Prompt Guide).
122
+
123
+ - **ChatGPT** (web/app): if your plan shows local-connector support, point a
124
+ connector at the same server. If it doesn't, skip this — use the
125
+ **paste-the-text** prompts in the Prompt Guide instead; everything except
126
+ reading PDFs directly works that way.
127
+
128
+ **Step 3 — Test it.** In your AI assistant, type:
129
+ > *"List the tor-to-proposal tools you have."*
130
+ If it lists twenty-ish tools with names like `bid_pack_start` and
131
+ `pricing_model`, you're connected.
132
+
133
+ ---
134
+
135
+ ## 5. Your first bid, step by step (what actually happens)
136
+
137
+ **Step 1 — The screen.** You say: *"Run a bid pack on this folder."* You get a
138
+ one-page summary: **title, reference number, deadline, how long the work is,
139
+ how they'll score you** (e.g. "70 points technical, 30 financial"), and red
140
+ flags. **You decide: go or no-go.**
141
+
142
+ **Step 2 — The one questionnaire.** The assistant puts all remaining questions
143
+ to you in a single numbered list. Typical questions: *"Your base day rate?"*
144
+ (or: *"Confirm your saved rate of $400 — yes/no"*), *"Can you start by 1
145
+ November?"*, *"The letter needs your exact availability."* You answer in one
146
+ reply. Pricing and the financial proposal are produced from YOUR answers,
147
+ every line showing its formula.
148
+
149
+ **Step 3 — Your tailored CV.** The tool re-orders your real CV so the
150
+ evidence the scorers want comes first, using the ToR's own words. It shows you
151
+ a green ✔ MATCH table and red **"GAP — do not claim"** lines. Those GAP lines
152
+ must not appear anywhere in your bid, no matter how tempting — the tool
153
+ enforces it.
154
+
155
+ **Step 4 — The drafts.** A one-page cover letter and a technical response with
156
+ **one section per scoring criterion**, quoting the ToR with page numbers.
157
+ Places it can't fill honestly appear as `[FILL]` — answer them and the drafts
158
+ are re-checked automatically.
159
+
160
+ **Step 5 — The self-score.** The tool scores your draft against their grid:
161
+ *"Criterion 3 (25 pts): 2 of 3 sub-elements covered; missing: sustainability
162
+ plan (p.7). 8.3 points at risk."* You fix, re-score, repeat until the at-risk
163
+ points are ~zero. **This is the step that wins points.**
164
+
165
+ **Step 6 — The pack.** The finished PDFs are read back and audited — the check
166
+ runs on the final documents, so a broken table or a leftover placeholder cannot
167
+ slip through. When it passes you get: the four documents, a submission
168
+ checklist, and `deadline.ics` for your calendar.
169
+
170
+ **Step 7 — Submit.** Through the channel and deadline **stated in the ToR**, in
171
+ their time zone. Your pricing worksheets are never included — they contain
172
+ your floor math.
173
+
174
+ **Step 8 — Save the outcome.** When the client announces results, tell your
175
+ assistant *"record this bid: won/lost"*. Over months this builds your private
176
+ database of what you quoted and what happened — the most valuable pricing data
177
+ you will ever own.
178
+
179
+ ---
180
+
181
+ ## 6. The honesty rules, in plain words
182
+
183
+ 1. **Everything traceable.** Every fact in your documents points to a page in
184
+ the ToR, something you supplied, or a public source with a link and date.
185
+ Otherwise it's a yellow `[FILL]`.
186
+ 2. **Nothing invented.** No fake rates, experience, dates or clients. The
187
+ tailored CV is verified against your master CV line by line — including
188
+ every number.
189
+ 3. **Their template wins.** Your documents are restructured into the client's
190
+ own forms — not the other way round.
191
+ 4. **No gut-feel prices.** A price is either your own floor calculation or a
192
+ cited market benchmark — and it's always YOUR number, confirmed.
193
+
194
+ ---
195
+
196
+ ## 7. If something goes wrong
197
+
198
+ | What you see | What it means | What to do |
199
+ | --- | --- | --- |
200
+ | "OCR required" | The PDF is a scanned photo, not text | Ask your assistant, or run it through a free OCR tool first, then retry |
201
+ | "missing dependency … run npm install" | Setup step 1 wasn't finished | Open Terminal in the tool's folder, type `npm install`, press Enter |
202
+ | "sign-off does not match CV name" | The letter's signature differs from your CV | Use the exact same name + credentials |
203
+ | "unanchored bullet" in the CV lint | The tailored CV has a line your master CV can't prove | Delete it, or give the true fact as an evidence answer and let it back in |
204
+ | "CHECK FAILED … [FILL] list" | A document still has empty required boxes | Give the tool the missing values, or fill those boxes by hand |
205
+ | "Pack NOT built" | The final check caught something | Read pack-report.md; fix each item; re-run. (Any old pack folder was removed so you can't submit a stale one.) |
206
+ | Exit code 2 / "do not submit" | A pre-flight check caught something | Read the list it gives you; fix each item; run again |
207
+ | Assistant invents a number | (Should not happen — but all AI can slip) | Reply: *"That number has no source. Mark it [FILL] and ask me."* |
208
+
209
+ ---
210
+
211
+ ## 8. Little dictionary
212
+
213
+ - **ToR** — Terms of Reference: the client's document describing the job.
214
+ - **Bid / proposal** — your offer: cover letter + CV + technical document + price.
215
+ - **Bid pack** — the finished folder: the four documents, the checklist, the
216
+ calendar file, the zip.
217
+ - **Consolidated questionnaire** — every open question in one numbered list,
218
+ asked once. The answer file is `answers.json`.
219
+ - **Compliance matrix** — a table of everything the ToR demands, with your
220
+ answer and status.
221
+ - **Evaluation grid / criteria** — the official scoring table; the bid is won here.
222
+ - **Master CV** — your full, real CV. The tailored one is derived from it and
223
+ verified against it.
224
+ - **`[FILL]`** — a yellow flag meaning "a real fact goes here — yours".
225
+ - **Floor rate** — the lowest daily rate that still pays your bills. Below it,
226
+ you're paying the client.
227
+ - **MCP / skill / plugin** — three plumbing formats for connecting the tool to
228
+ AI assistants. You only care that it's connected.
@@ -0,0 +1,45 @@
1
+ # Bid library schema — the win/loss corpus
2
+
3
+ Opt-in, local by default, anonymizable. This corpus is what eventually turns
4
+ rate bands and "what evaluators reward" from guesses into data. Schema ships
5
+ now so every completed bid can be recorded from day one.
6
+
7
+ ```
8
+ library/bids/<reference-number>/
9
+ ├── tor-extract.json # frozen copy of out/tor-extract.json at submission time
10
+ ├── outcome.json # see below
11
+ ├── feedback.md # evaluator comments, verbatim if received (client name redactable)
12
+ └── rate-band.json # what you quoted and which band you used
13
+ ```
14
+
15
+ ## outcome.json
16
+
17
+ ```json
18
+ {
19
+ "referenceNumber": "UNDP-RFP-2026-042",
20
+ "client": "UNDP",
21
+ "sector": "M&E",
22
+ "region": "East Africa",
23
+ "submittedOn": "2026-09-29",
24
+ "outcome": "won", // won | lost | withdrawn
25
+ "decidedOn": "[FILL]",
26
+ "feeQuoted": 9312.50,
27
+ "currency": "USD",
28
+ "basis": "day",
29
+ "bandUsed": { "low": 380, "high": 520, "source": "market-rates-research.md row 2", "n": null },
30
+ "winningFeeIfDisclosed": "[FILL]",
31
+ "notes": "[FILL: anything the evaluator said, verbatim]"
32
+ }
33
+ ```
34
+
35
+ ## Rules
36
+
37
+ - Recording a bid is opt-in; ask the user before creating the folder.
38
+ - Never store evaluator personal data beyond their official role.
39
+ - The client may be redacted to type (UN agency / ministry / INGO) at the
40
+ user's choice.
41
+ - Aggregate analytics (rate bands by client × sector × region) are only ever
42
+ reported where n ≥ 10, always with n and date. Below that, no band is
43
+ printed — no false precision, no leaking small-n identities.
44
+ - `library/bids/` is gitignored by default; if the user ever opts into a
45
+ shared corpus, only anonymized aggregates leave the machine.
File without changes
package/mcp/server.mjs ADDED
@@ -0,0 +1,358 @@
1
+ #!/usr/bin/env node
2
+ // MCP stdio server: exposes the tor-to-proposal CLI as MCP tools so Claude
3
+ // Desktop, Claude Code, Codex, ChatGPT (developer mode) and any MCP client can
4
+ // use them natively. Zero dependencies: JSON-RPC 2.0, newline-delimited messages.
5
+ //
6
+ // Convention for every tool: pass ABSOLUTE paths for files you read AND for
7
+ // outputs (the server's working directory belongs to the host app, not you).
8
+ // Exit codes: 0 ok | 1 validation/hard-fail | 2 cannot-process (e.g. OCR
9
+ // needed, inverted band) | 3 dependencies missing (run `npm install` in the
10
+ // tor-to-proposal repo).
11
+ import { spawnSync } from 'node:child_process';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+ import readline from 'node:readline';
15
+
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ const BIN = path.join(ROOT, 'bin');
18
+
19
+ const P = (description) => ({ type: 'string', description });
20
+ const N = (description) => ({ type: 'number', description });
21
+ const S = (properties, required = []) => ({ type: 'object', properties, required });
22
+ const ABS_OUT = 'Absolute output path (the server cwd belongs to the host app — always pass absolute paths).';
23
+
24
+ const TOOLS = [
25
+ {
26
+ name: 'pdf_extract',
27
+ description:
28
+ 'Step 0 — Extract page-tagged text from a ToR document (PDF, .docx, .xlsx, or already-extracted .txt). Inserts [[PAGE n]] / [[SHEET n]] markers that every later tool uses for citation. Exit 2 means the PDF is likely scanned: run OCR (tesseract) first, do not guess page numbers.',
29
+ inputSchema: S({
30
+ file: P('Absolute path to the ToR document'),
31
+ out: P('Optional ' + ABS_OUT + ' Omit to receive the text in the response.'),
32
+ signals: P('Optional ' + ABS_OUT + ' (extraction signals JSON: pages, scannedLikely, hasEvaluationTable…).'),
33
+ }, ['file']),
34
+ argv: (a) => [a.file, ...(a.out ? ['--out', a.out] : []), ...(a.signals ? ['--signals', a.signals] : [])],
35
+ script: 'pdf-extract.mjs',
36
+ },
37
+ {
38
+ name: 'extract_tor',
39
+ description:
40
+ 'Step 1 — Structure a page-tagged ToR into tor-extract.json + bid-screen.md + compliance-matrix.md. Extracts reference number, deadline, evaluation criteria with weights (checks they sum to the stated total), deliverables, payment terms, submission channel. Optionally computes expected-value go/no-go if fee/bid_days/day_rate are supplied (user-confirmed numbers only).',
41
+ inputSchema: S({
42
+ tor: P('Absolute path to the page-tagged text (from pdf_extract)'),
43
+ out_dir: P('Optional ' + ABS_OUT + ' (directory). Default: out/ under the server cwd.'),
44
+ json: P('Optional absolute path to agent-extracted fields ({"deadline": {"value": "...", "page": 4}, ...}) — accepted only with page attribution'),
45
+ fee: N('User-confirmed expected fee for EV analysis'),
46
+ bid_days: N('User-confirmed bid preparation days for EV analysis'),
47
+ day_rate: N('User-confirmed day rate for EV analysis'),
48
+ }, ['tor']),
49
+ argv: (a) => [
50
+ '--tor', a.tor, '--out-dir', a.out_dir || 'out',
51
+ ...(a.json ? ['--json', a.json] : []),
52
+ ...(a.fee !== undefined ? ['--fee', a.fee, '--bid-days', a.bid_days, '--day-rate', a.day_rate] : []),
53
+ ],
54
+ script: 'extract.mjs',
55
+ },
56
+ {
57
+ name: 'cv_gap',
58
+ description:
59
+ 'Step 3 — Compare a CV against the ToR requirements. MATCH rows are evidence found in the CV; GAP rows must never be claimed anywhere in the proposal.',
60
+ inputSchema: S({
61
+ tor: P('Absolute path to the page-tagged ToR text'),
62
+ cv: P('Absolute path to the CV text file'),
63
+ out: P('Optional ' + ABS_OUT),
64
+ }, ['tor', 'cv']),
65
+ argv: (a) => ['--tor', a.tor, '--cv', a.cv, ...(a.out ? ['--out', a.out] : [])],
66
+ script: 'cv-gap.mjs',
67
+ },
68
+ {
69
+ name: 'cover_letter_lint',
70
+ description:
71
+ 'Step 6 — Lint a drafted cover letter: bans superlatives, requires page-tagged claims, flags [FILL] leftovers, verifies the sign-off matches the CV name, checks length. Exit 1 = fix the draft and lint again.',
72
+ inputSchema: S({
73
+ draft: P('Absolute path to the drafted cover letter (markdown)'),
74
+ cv: P('Absolute path to the CV text file'),
75
+ gap: P('Optional absolute path to the cv-gap report — mentions of GAP skills then fail the lint'),
76
+ max_words: N('Optional word limit (default 550 ≈ 1 page)'),
77
+ report: P('Optional ' + ABS_OUT),
78
+ }, ['draft', 'cv']),
79
+ argv: (a) => ['lint', '--draft', a.draft, '--cv', a.cv,
80
+ ...(a.gap ? ['--gap', a.gap] : []), ...(a.max_words ? ['--max-words', a.max_words] : []),
81
+ ...(a.report ? ['--report', a.report] : [])],
82
+ script: 'cover-letter.mjs',
83
+ },
84
+ {
85
+ name: 'market_rates_scaffold',
86
+ description:
87
+ 'Step 4a — Generate market-rates-research.md: a benchmark table (UNDP/UNICEF fee grids, UNGM award notices, surveys, postings) where EVERY rate must be researched and cited with source + date. Contains no numbers by design.',
88
+ inputSchema: S({
89
+ title: P('Assignment title + reference number'),
90
+ currency: P('ISO currency code (default USD)'),
91
+ out: P('Optional ' + ABS_OUT),
92
+ }),
93
+ argv: (a) => ['scaffold', ...(a.title ? ['--title', a.title] : []), ...(a.currency ? ['--currency', a.currency] : []), ...(a.out ? ['--out', a.out] : [])],
94
+ script: 'market-rates.mjs',
95
+ },
96
+ {
97
+ name: 'market_rates_position',
98
+ description:
99
+ 'Step 4b — Position a rate against a cited benchmark band: BELOW/WITHIN/ABOVE + advisory. Refuses an inverted band (low >= high) with exit 2.',
100
+ inputSchema: S({
101
+ your_rate: N('The rate being evaluated'),
102
+ benchmark_low: N('Low end of the cited band (from researched sources)'),
103
+ benchmark_high: N('High end of the cited band'),
104
+ currency: P('ISO currency code (default USD)'),
105
+ }, ['your_rate', 'benchmark_low', 'benchmark_high']),
106
+ argv: (a) => ['position', '--your-rate', a.your_rate, '--benchmark-low', a.benchmark_low, '--benchmark-high', a.benchmark_high, ...(a.currency ? ['--currency', a.currency] : [])],
107
+ script: 'market-rates.mjs',
108
+ },
109
+ {
110
+ name: 'pricing_model',
111
+ description:
112
+ 'Step 5 — GATED: call only after the user confirmed base rate, loading and contingency as THEIR numbers. Computes every billing format with visible formulas, the loaded rate, quote floor, lump-sum and assignment total; writes pricing.json (input for template_filler).',
113
+ inputSchema: S({
114
+ base: N('Base rate in the chosen basis'),
115
+ basis: { type: 'string', enum: ['hour', 'day', 'week', 'month'], description: 'Unit of the base rate' },
116
+ currency: P('ISO currency code (default USD)'),
117
+ loading: N('Overhead multiplier as decimal (e.g. 0.25 = 25%)'),
118
+ contingency: N('Risk multiplier as decimal (e.g. 0.10 = 10%)'),
119
+ effort: N('Estimated person-days (for lump-sum)'),
120
+ weeks: N('Assignment weeks (for assignment total)'),
121
+ out_dir: P('Optional ' + ABS_OUT + ' (directory). Default: out/ under the server cwd.'),
122
+ }, ['base', 'basis']),
123
+ argv: (a) => ['--base', a.base, '--basis', a.basis,
124
+ ...(a.currency ? ['--currency', a.currency] : []), ...(a.loading !== undefined ? ['--loading', a.loading] : []),
125
+ ...(a.contingency !== undefined ? ['--contingency', a.contingency] : []), ...(a.effort !== undefined ? ['--effort', a.effort] : []),
126
+ ...(a.weeks !== undefined ? ['--weeks', a.weeks] : []), ...(a.out_dir ? ['--out-dir', a.out_dir] : [])],
127
+ script: 'pricing-model.mjs',
128
+ },
129
+ {
130
+ name: 'dossier',
131
+ description:
132
+ 'Step 2 — Scaffold a client dossier (type preset, registration prerequisites, payment reputation, past awards). You fill it only from citable sources; [FILL] rows stay until researched.',
133
+ inputSchema: S({
134
+ client: P('Client name (e.g. "UNOPS", "Ministry of Finance")'),
135
+ sector: P('Sector keyword (e.g. "M&E")'),
136
+ out: P('Optional ' + ABS_OUT),
137
+ }, ['client']),
138
+ argv: (a) => ['--client', a.client, ...(a.sector ? ['--sector', a.sector] : []), ...(a.out ? ['--out', a.out] : [])],
139
+ script: 'dossier.mjs',
140
+ },
141
+ {
142
+ name: 'simulate',
143
+ description:
144
+ 'Step 7 — Reverse-score a drafted technical response against the ToR evaluation grid. Output: at-risk points per criterion (= weight × missing share), missing sub-elements verbatim, rewrite priorities. Coverage arithmetic, NOT a predicted evaluator score.',
145
+ inputSchema: S({
146
+ extract: P('Absolute path to tor-extract.json'),
147
+ response: P('Absolute path to the drafted technical response (markdown)'),
148
+ sub_elements: P('Optional absolute path to sub-elements JSON ({"criterion label": ["sub-element 1", ...]} extracted verbatim from the ToR)'),
149
+ out: P('Optional ' + ABS_OUT),
150
+ }, ['extract', 'response']),
151
+ argv: (a) => ['--extract', a.extract, '--response', a.response, ...(a.sub_elements ? ['--sub-elements', a.sub_elements] : []), ...(a.out ? ['--out', a.out] : [])],
152
+ script: 'simulator.mjs',
153
+ },
154
+ {
155
+ name: 'template_filler',
156
+ description:
157
+ "Step 8 — Fill the CLIENT'S OWN forms. map: list fillable placeholders/labeled cells; fill: write values into a copy of the template (never alters structure, never overwrites formulas); check: exit 1 while any required field has no value ([FILL] list). data is a flat label->value map (build it from pricing.json + tor-extract.json, labels exactly as `map` reports).",
158
+ inputSchema: S({
159
+ command: { type: 'string', enum: ['map', 'fill', 'check'], description: 'map = list fields; fill = write values; check = verify completeness' },
160
+ template: P('Absolute path to the client .docx or .xlsx form'),
161
+ data: P('Absolute path to the flat JSON data map'),
162
+ out: P('For fill: ' + ABS_OUT),
163
+ report: P('Optional ' + ABS_OUT + ' (fill-report.md tracing every write)'),
164
+ }, ['command', 'template', 'data']),
165
+ argv: (a) => [a.command, '--template', a.template, '--data', a.data, ...(a.out ? ['--out', a.out] : []), ...(a.report ? ['--report', a.report] : [])],
166
+ script: 'template-filler.mjs',
167
+ },
168
+ {
169
+ name: 'audit',
170
+ description:
171
+ 'Step 9 — Pre-submission hard-fail gate: [FILL] leftovers, compliance-matrix rows not Complete, and claim-shaped sentences with no trace ([[PAGE n]] / p.X / URL / user-input). Exit 1 = do not submit yet.',
172
+ inputSchema: S({
173
+ proposal: P('Absolute path to the final proposal (markdown)'),
174
+ matrix: P('Optional absolute path to compliance-matrix.md'),
175
+ extras: { type: 'array', items: { type: 'string' }, description: 'Optional additional files to audit (cover letter, annexes) — absolute paths' },
176
+ report: P('Optional ' + ABS_OUT),
177
+ }, ['proposal']),
178
+ argv: (a) => ['--proposal', a.proposal, ...(a.matrix ? ['--matrix', a.matrix] : []),
179
+ ...(a.extras ? [].concat(a.extras).flatMap((e) => ['--extras', e]) : []), ...(a.report ? ['--report', a.report] : [])],
180
+ script: 'audit.mjs',
181
+ },
182
+ // ---------------- v2: the bid pack ----------------
183
+ {
184
+ name: 'profile_set',
185
+ description:
186
+ 'One-time consultant profile (stored at ~/.tor-to-proposal/profile.json). Set dotted fields: identity.name, identity.credentials, identity.email, identity.phone, identity.title, identity.location, identity.nationality, identity.languages (comma string), rates.floor.annualIncome, rates.floor.billableDays, rates.floor.costLoading, rates.defaults.base/basis/loading/contingency/currency, cv.masterPath. Every value is USER-SUPPLIED — never guess one.',
187
+ inputSchema: S({
188
+ pairs: { type: 'array', items: { type: 'string' }, description: 'key=value pairs, e.g. "identity.name=Ayesha Khan", "rates.defaults.base=400"' },
189
+ }, ['pairs']),
190
+ argv: (a) => ['set', ...[].concat(a.pairs)],
191
+ script: 'profile.mjs',
192
+ },
193
+ {
194
+ name: 'profile_get',
195
+ description: 'Read the consultant profile (whole, or one dotted key). Returns an error if no profile exists — then offer to run profile_set with the user.',
196
+ inputSchema: S({ key: P('Optional dotted key, e.g. rates.defaults.base') }),
197
+ argv: (a) => ['get', ...(a.key ? [a.key] : [])],
198
+ script: 'profile.mjs',
199
+ },
200
+ {
201
+ name: 'bid_pack_start',
202
+ description:
203
+ 'Bid pack step 1 — the one-shot intake. Runs pdf-extract -> extract (bid screen + compliance matrix) -> cv-gap -> cv-tailor on the given ToR + CV, then writes out/questions.md: the CONSOLIDATED questionnaire. Put every listed question to the user in ONE message; write their answers to answers.json; call bid_pack_apply.',
204
+ inputSchema: S({
205
+ tor: P('Absolute path to the ToR document (pdf/docx/xlsx/txt)'),
206
+ cv: P('Absolute path to the CV (pdf/docx/txt) — recommended'),
207
+ dir: P('Absolute bid directory (default: <tor-dir>/bid)'),
208
+ no_cv_tailor: { type: 'boolean', description: 'Skip the automatic tailored-CV draft' },
209
+ }, ['tor']),
210
+ argv: (a) => ['start', '--tor', a.tor, ...(a.cv ? ['--cv', a.cv] : []), '--dir', a.dir || path.join(path.dirname(a.tor), 'bid'),
211
+ ...(a.no_cv_tailor ? ['--no-cv-tailor'] : [])],
212
+ script: 'bid-pack.mjs',
213
+ },
214
+ {
215
+ name: 'bid_pack_apply',
216
+ description:
217
+ 'Bid pack step 2 — record the user\'s answers (answers.json: {"Q-PRICING-BASE": 450, "Q-AVAILABILITY": "2026-11-01", ...}); runs pricing + financial proposal when pricing numbers are present, records CV evidence and draft-fill answers, and regenerates the open-question list. Repeat until 0 open.',
218
+ inputSchema: S({
219
+ answers: P('Absolute path to answers.json (flat map question-id -> answer)'),
220
+ dir: P('Absolute bid directory (the one used in bid_pack_start)'),
221
+ }, ['answers', 'dir']),
222
+ argv: (a) => ['apply', '--answers', a.answers, '--dir', a.dir],
223
+ script: 'bid-pack.mjs',
224
+ },
225
+ {
226
+ name: 'bid_pack_pack',
227
+ description:
228
+ 'Bid pack final step — verifies every final document (cover letter, CV, technical, financial) by RE-EXTRACTING its text (PDF/docx/xlsx) and re-running the audit on those bytes, then builds pack/ + submission-checklist.md + deadline.ics + the zip. Exit 1 = do not submit. fallback_pdf renders md-only docs with the built-in renderer; prefer rendering with your own document tooling first.',
229
+ inputSchema: S({
230
+ dir: P('Absolute bid directory'),
231
+ fallback_pdf: { type: 'boolean', description: 'Render md-only docs to PDF with the built-in fallback renderer' },
232
+ no_zip: { type: 'boolean', description: 'Skip the zip' },
233
+ }, ['dir']),
234
+ argv: (a) => ['pack', '--dir', a.dir, ...(a.fallback_pdf ? ['--fallback-pdf'] : []), ...(a.no_zip ? ['--no-zip'] : [])],
235
+ script: 'bid-pack.mjs',
236
+ },
237
+ {
238
+ name: 'cv_tailor_build',
239
+ description:
240
+ 'Build a ToR-tailored CV draft: reorders/selects master-CV bullets against the evaluation weights. Adds NO facts — every output line comes from the master CV. Then run cv_tailor_lint.',
241
+ inputSchema: S({
242
+ cv: P('Absolute path to the master CV (txt/md)'),
243
+ extract: P('Absolute path to tor-extract.json'),
244
+ profile: P('Optional absolute path to profile.json (identity block)'),
245
+ gap: P('Optional absolute path to cv-gap-report.md (GAP discipline)'),
246
+ out: P('Optional ' + ABS_OUT),
247
+ report: P('Optional ' + ABS_OUT),
248
+ }, ['cv', 'extract']),
249
+ argv: (a) => ['build', '--cv', a.cv, '--extract', a.extract, ...(a.profile ? ['--profile', a.profile] : []), ...(a.gap ? ['--gap', a.gap] : []), ...(a.out ? ['--out', a.out] : []), ...(a.report ? ['--report', a.report] : [])],
250
+ script: 'cv-tailor.mjs',
251
+ },
252
+ {
253
+ name: 'cv_tailor_lint',
254
+ description:
255
+ 'Verify a tailored CV: every bullet must anchor to the master CV (token containment + all numbers preserved) or come from cv-evidence.json (user-input answers). GAP terms hard-fail. Exit 1 = fix before use.',
256
+ inputSchema: S({
257
+ cv: P('Absolute path to the tailored CV'),
258
+ master: P('Absolute path to the ORIGINAL master CV'),
259
+ gap: P('Optional absolute path to cv-gap-report.md'),
260
+ evidence: P('Optional absolute path to cv-evidence.json (user-input facts)'),
261
+ report: P('Optional ' + ABS_OUT),
262
+ }, ['cv', 'master']),
263
+ argv: (a) => ['lint', '--cv', a.cv, '--master', a.master, ...(a.gap ? ['--gap', a.gap] : []), ...(a.evidence ? ['--evidence', a.evidence] : []), ...(a.report ? ['--report', a.report] : [])],
264
+ script: 'cv-tailor.mjs',
265
+ },
266
+ {
267
+ name: 'financial_proposal',
268
+ description:
269
+ 'Render the client-facing financial proposal (md + xlsx with visible formulas) from pricing.json + tor-extract.json. Numbers only from user-confirmed pricing; the internal floor math stays in pricing.md.',
270
+ inputSchema: S({
271
+ pricing: P('Absolute path to pricing.json'),
272
+ extract: P('Absolute path to tor-extract.json'),
273
+ context: P('Optional absolute path to bid-context.json (consultant, validity, reimbursables, taxes)'),
274
+ out_dir: P('Optional ' + ABS_OUT + ' (directory)'),
275
+ xlsx: { type: 'boolean', description: 'Also write financial-proposal.xlsx' },
276
+ }, ['pricing', 'extract']),
277
+ argv: (a) => ['--pricing', a.pricing, '--extract', a.extract, ...(a.context ? ['--context', a.context] : []), ...(a.out_dir ? ['--out-dir', a.out_dir] : []), ...(a.xlsx ? ['--xlsx'] : [])],
278
+ script: 'financial-proposal.mjs',
279
+ },
280
+ {
281
+ name: 'render_pdf',
282
+ description:
283
+ 'Fallback renderer: markdown -> plain submittable PDF (headings, bullets, tables, page numbers). Use when no other document tooling is available; package_bid verifies whichever PDF it finds.',
284
+ inputSchema: S({
285
+ input: P('Absolute path to the markdown file'),
286
+ out: P(ABS_OUT),
287
+ footer: P('Optional footer text (the reference number works well)'),
288
+ }, ['input', 'out']),
289
+ argv: (a) => [a.input, '--out', a.out, ...(a.footer ? ['--footer', a.footer] : [])],
290
+ script: 'render.mjs',
291
+ },
292
+ {
293
+ name: 'package_bid',
294
+ description:
295
+ 'Verify + package a finished bid directory: inventories the four documents, re-extracts rendered files and re-audits, writes pack-report.md, submission-checklist.md, deadline.ics, pack/ and the zip. Exit 0 = pack ready; exit 1 = do not submit.',
296
+ inputSchema: S({
297
+ dir: P('Absolute bid directory'),
298
+ fallback_pdf: { type: 'boolean', description: 'Render md-only docs with the built-in fallback renderer' },
299
+ no_zip: { type: 'boolean' },
300
+ }, ['dir']),
301
+ argv: (a) => ['--dir', a.dir, ...(a.fallback_pdf ? ['--fallback-pdf'] : []), ...(a.no_zip ? ['--no-zip'] : [])],
302
+ script: 'package.mjs',
303
+ },
304
+ ];
305
+
306
+ function callTool(name, args) {
307
+ const tool = TOOLS.find((t) => t.name === name);
308
+ if (!tool) return { text: `unknown tool: ${name}`, isError: true };
309
+ let argv;
310
+ try { argv = tool.argv(args ?? {}); } catch (e) { return { text: `bad arguments: ${e.message}`, isError: true }; }
311
+ const r = spawnSync(process.execPath, [path.join(BIN, tool.script), ...argv.map(String)], { encoding: 'utf8' });
312
+ const parts = [];
313
+ if (r.stdout?.trim()) parts.push(r.stdout.trim());
314
+ if (r.stderr?.trim()) parts.push(`— stderr —\n${r.stderr.trim()}`);
315
+ if (r.status !== 0) parts.push(`(exit code ${r.status})`);
316
+ return { text: parts.join('\n') || '(no output)', isError: r.status !== 0 };
317
+ }
318
+
319
+ function handle(msg) {
320
+ const { id, method, params } = msg;
321
+ if (method === 'initialize') {
322
+ return {
323
+ protocolVersion: typeof params?.protocolVersion === 'string' ? params.protocolVersion : '2024-11-05',
324
+ capabilities: { tools: {} },
325
+ serverInfo: { name: 'tor-to-proposal', version: '2.0.0' },
326
+ };
327
+ }
328
+ if (typeof method === 'string' && method.startsWith('notifications/')) return undefined;
329
+ if (method === 'ping') return {};
330
+ if (method === 'tools/list') return { tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })) };
331
+ if (method === 'tools/call') {
332
+ const r = callTool(params?.name, params?.arguments);
333
+ return { content: [{ type: 'text', text: r.text }], ...(r.isError ? { isError: true } : {}) };
334
+ }
335
+ if (id !== undefined) return { error: { code: -32601, message: `method not found: ${method}` } };
336
+ return undefined;
337
+ }
338
+
339
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
340
+ rl.on('line', (line) => {
341
+ const t = line.trim();
342
+ if (!t) return;
343
+ let msg;
344
+ try { msg = JSON.parse(t); } catch {
345
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }) + '\n');
346
+ return;
347
+ }
348
+ try {
349
+ const result = handle(msg);
350
+ if (result === undefined) return;
351
+ const response = { jsonrpc: '2.0', id: msg.id, ...(result.error ? { error: result.error } : { result }) };
352
+ process.stdout.write(JSON.stringify(response) + '\n');
353
+ } catch (e) {
354
+ if (msg.id !== undefined) {
355
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: msg.id, error: { code: -32603, message: String(e?.message || e) } }) + '\n');
356
+ }
357
+ }
358
+ });