tor-to-proposal 2.0.1 → 2.1.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.
package/SKILL.md CHANGED
@@ -82,9 +82,13 @@ Go/no-go is THEIR call. If no-go: stop and say so.
82
82
  ### Phase 2 — The ONE questionnaire (no scattered questions, ever)
83
83
 
84
84
  `out/questions.md` consolidates every open decision: pricing, availability,
85
- validity, draft `[FILL]`s, CV evidence. **Put all of them to the user in one
86
- message**, grouped. Never drip-feed questions and never answer one for the
87
- user. Then:
85
+ validity, draft `[FILL]`s, CV evidence. Two routes, in order of preference:
86
+
87
+ 1. **Interactive wizard (non-technical users):** `node bin/bid-pack.mjs ask
88
+ --dir <bid-dir>` — asks each open question in the terminal, saves
89
+ answers.json and applies it. Run again; it only asks what is still open.
90
+ 2. **Assistant-mediated:** put all questions to the user in one message,
91
+ never answer one for the user, then:
88
92
 
89
93
  ```bash
90
94
  # write the answers verbatim:
package/bin/bid-pack.mjs CHANGED
@@ -5,6 +5,9 @@
5
5
  //
6
6
  // start --tor <file> [--cv <file>] [--dir bid/] extract + screen + cv-gap
7
7
  // + cv-tailor build + profile import -> questions.json/questions.md
8
+ // ask [--dir bid/] interactive wizard: answers
9
+ // the open questions one at a time in the terminal, writes answers.json
10
+ // and applies them (no editor or JSON knowledge needed)
8
11
  // apply --answers answers.json [--dir bid/] store answers, run pricing,
9
12
  // record evidence + fill answers, regenerate the open-question list
10
13
  // pack [--dir bid/] [--fallback-pdf] [--no-zip] verify + audit + zip (-> package.mjs)
@@ -14,6 +17,7 @@
14
17
  import fs from 'node:fs';
15
18
  import path from 'node:path';
16
19
  import crypto from 'node:crypto';
20
+ import readline from 'node:readline/promises';
17
21
  import { spawnSync } from 'node:child_process';
18
22
  import { fileURLToPath } from 'node:url';
19
23
  import { fail, parseArgs, readText, writeOut, helpText, mdTable } from './lib.mjs';
@@ -21,19 +25,23 @@ import { loadProfile, profilePath } from './profile.mjs';
21
25
 
22
26
  const HELP = helpText('bid-pack', [
23
27
  'start --tor <file.(pdf|docx|txt)> [--cv <file>] [--dir bid/] [--profile] [--no-cv-tailor]',
28
+ 'ask [--dir bid/]',
24
29
  'apply --answers answers.json [--dir bid/]',
25
30
  'pack [--dir bid/] [--fallback-pdf] [--no-zip]',
26
31
  '',
27
32
  'start runs every mechanical step (pdf-extract -> extract -> cv-gap -> cv-tailor)',
28
33
  'and consolidates ALL open questions into out/questions.md. Answer them in one go,',
29
34
  'have your assistant write answers.json, then apply. Repeat apply until 0 open.',
35
+ 'ask is the human-friendly route: an interactive wizard that asks each open',
36
+ 'question in the terminal (Enter = suggested default, "back" to redo, "quit" to',
37
+ 'cancel), saves answers.json and applies it — no editor or JSON needed.',
30
38
  'pack verifies the final documents (re-extract + re-audit) and builds the zip.',
31
39
  ]);
32
40
 
33
41
  const HERE = path.dirname(fileURLToPath(import.meta.url));
34
42
  const args = parseArgs(process.argv.slice(2));
35
43
  const cmd = args._[0];
36
- if (args.__help || !['start', 'apply', 'pack'].includes(cmd)) {
44
+ if (args.__help || !['start', 'ask', 'apply', 'pack'].includes(cmd)) {
37
45
  process.stdout.write(HELP);
38
46
  process.exit(args.__help ? 0 : 1);
39
47
  }
@@ -144,6 +152,21 @@ function writeQuestions(q) {
144
152
  writeOut(path.join(outDir, 'questions.md'), md);
145
153
  }
146
154
 
155
+ // Human-readable PDF companions for the working files a non-technical user is
156
+ // most likely to be pointed at (regenerated after start/apply; the .md stays
157
+ // the source of truth — the PDFs are for reading, not editing).
158
+ function renderPreviews() {
159
+ for (const [file, label] of [
160
+ ['bid-screen.md', 'Bid-Screen'],
161
+ ['questions.md', 'Questions'],
162
+ ['cv-gap-report.md', 'CV-Gap-Report'],
163
+ ['cv-tailor-report.md', 'CV-Tailor-Report'],
164
+ ]) {
165
+ if (!inDir(file)) continue;
166
+ run('render.mjs', [path.join(outDir, file), '--out', path.join(outDir, 'preview', `${label}.pdf`), '--footer', `preview of out/${file} — the .md file is the source of truth`], { label: `render preview ${label}` });
167
+ }
168
+ }
169
+
147
170
  // ---------------- context ----------------
148
171
  function writeContext() {
149
172
  const extractPath = inDir('tor-extract.json');
@@ -214,6 +237,7 @@ function start() {
214
237
  writeQuestions(q);
215
238
  state.steps.questions = { status: 'done', open: q.openCount };
216
239
  writeState(state);
240
+ renderPreviews();
217
241
 
218
242
  // report
219
243
  const extract = JSON.parse(fs.readFileSync(path.join(outDir, 'tor-extract.json'), 'utf8'));
@@ -226,7 +250,8 @@ function start() {
226
250
  `- bid-screen.md — go/no-go decision (the USER decides; read it together)\n` +
227
251
  `- compliance-matrix.md, tor-extract.json\n` +
228
252
  (args.cv ? `- cv-gap-report.md, cv-tailored.md (+ report)\n` : '') +
229
- `- questions.json / questions.md — **${q.items.length} open question(s)**\n\n` +
253
+ `- questions.json / questions.md — **${q.items.length} open question(s)**\n` +
254
+ `- preview/ — readable PDFs of the bid screen + questionnaire (just double-click)\n\n` +
230
255
  `## Next\n1. Review bid-screen.md with the user. No-go -> stop here.\n` +
231
256
  `2. Put every open question to the user IN ONE MESSAGE (questions.md has them).\n` +
232
257
  `3. Write answers.json {"Q-...": value, ...} -> bid-pack apply --answers answers.json --dir ${dir}\n` +
@@ -238,7 +263,11 @@ function start() {
238
263
 
239
264
  // ---------------- apply ----------------
240
265
  function apply() {
241
- if (!args.answers) { process.stdout.write(HELP); process.exit(1); }
266
+ if (!args.answers) {
267
+ process.stdout.write(HELP);
268
+ process.stdout.write(`\nTip: run "bid-pack ask --dir ${dir}" to answer the open questions interactively in this terminal — no JSON file needed.\n`);
269
+ process.exit(1);
270
+ }
242
271
  const answersPath = path.resolve(String(args.answers));
243
272
  const answers = JSON.parse(readText(answersPath, '--answers'));
244
273
  const qPath = inDir('questions.json');
@@ -326,6 +355,7 @@ function apply() {
326
355
  state.steps.questions = { status: 'done', open: fresh.openCount };
327
356
  state.steps.pricing = { status: inDir('pricing.json') ? 'done' : 'awaiting-user-numbers' };
328
357
  writeState(state);
358
+ renderPreviews();
329
359
 
330
360
  process.stdout.write(
331
361
  `# Apply: ${applied} answer(s) recorded, ${fresh.openCount} still open\n\n` +
@@ -335,6 +365,85 @@ function apply() {
335
365
  );
336
366
  }
337
367
 
368
+ // ---------------- ask (interactive question wizard) ----------------
369
+ async function askWizard() {
370
+ const qPath = inDir('questions.json');
371
+ if (!qPath) fail(`no questions.json in ${dir}/out — run: bid-pack start --tor <file> --dir ${dir}`, 2);
372
+ const q = JSON.parse(fs.readFileSync(qPath, 'utf8'));
373
+ const yours = q.items.filter((i) => i.type !== 'client');
374
+ const clients = q.items.filter((i) => i.type === 'client');
375
+ if (!yours.length) {
376
+ process.stdout.write(
377
+ (clients.length
378
+ ? `Nothing for you to answer here — but send these to the CLIENT before the cutoff:\n${clients.map((c) => `- ${c.question}`).join('\n')}\n`
379
+ : 'No open questions — everything is already resolved.\n') +
380
+ (inDir('pricing.json') ? `\nNext: bid-pack pack --dir ${dir} [--fallback-pdf]\n` : '')
381
+ );
382
+ return;
383
+ }
384
+
385
+ // Same terminal behavior as the profile wizard: interactive when run by a
386
+ // human; piped lines are consumed in order (automation/tests).
387
+ const piped = !process.stdin.isTTY;
388
+ let pipedLines = [];
389
+ if (piped) {
390
+ try {
391
+ const raw = fs.readFileSync(0, 'utf8');
392
+ pipedLines = raw.trim() ? raw.split('\n').map((l) => l.trim()) : [];
393
+ } catch { /* empty stdin */ }
394
+ }
395
+ let pipedPos = 0;
396
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
397
+ const askLine = async (prompt) => {
398
+ if (piped) return pipedPos < pipedLines.length ? pipedLines[pipedPos++] : 'EOF';
399
+ try {
400
+ return (await rl.question(prompt)).trim();
401
+ } catch (err) {
402
+ if (err && (err.code === 'ABORT_ERR' || err.name === 'AbortError')) return 'EOF';
403
+ throw err;
404
+ }
405
+ };
406
+
407
+ process.stdout.write(
408
+ `Bid questions — ${yours.length} to answer, one at a time.\n` +
409
+ 'Press Enter to accept the [suggested] value when one is shown. Type "back" to redo the previous question, "quit" to cancel.\n\n'
410
+ );
411
+
412
+ const answers = {};
413
+ let i = 0;
414
+ while (i < yours.length) {
415
+ const item = yours[i];
416
+ const cur = item.default !== undefined && item.default !== null ? String(item.default) : '';
417
+ const tail = item.unit ? ` (${item.unit})` : '';
418
+ const a = await askLine(`(${i + 1}/${yours.length}) ${item.question}${tail}${cur ? ` [${cur}]` : ''}\n> `);
419
+ if (a === 'EOF' || a.toLowerCase() === 'quit') {
420
+ rl.close();
421
+ process.stdout.write('\nCancelled — nothing was saved. (Your documents are untouched; run bid-pack ask again anytime.)\n');
422
+ return;
423
+ }
424
+ if (a.toLowerCase() === 'back') {
425
+ if (i === 0) { process.stdout.write(' (already at the first question)\n'); continue; }
426
+ const prev = yours[i - 1];
427
+ delete answers[prev.id];
428
+ i -= 1;
429
+ continue;
430
+ }
431
+ if (a === '' && cur === '') {
432
+ process.stdout.write(' This one needs an answer from you (no suggested default). Type it, or "quit" to stop.\n');
433
+ continue;
434
+ }
435
+ answers[item.id] = a === '' ? item.default : a;
436
+ i += 1;
437
+ }
438
+ rl.close();
439
+
440
+ const answersPath = path.join(dir, 'answers.json');
441
+ fs.writeFileSync(answersPath, JSON.stringify(answers, null, 2));
442
+ process.stdout.write(`\nSaved your ${Object.keys(answers).length} answer(s) to ${answersPath}\nApplying them now...\n\n`);
443
+ args.answers = answersPath;
444
+ apply();
445
+ }
446
+
338
447
  // ---------------- pack ----------------
339
448
  function pack() {
340
449
  const passThrough = ['--dir', dir, ...(args['fallback-pdf'] ? ['--fallback-pdf'] : []), ...(args['no-zip'] ? ['--no-zip'] : []), ...(args.profile ? ['--profile', String(args.profile)] : [])];
@@ -343,5 +452,6 @@ function pack() {
343
452
  }
344
453
 
345
454
  if (cmd === 'start') start();
455
+ else if (cmd === 'ask') await askWizard();
346
456
  else if (cmd === 'apply') apply();
347
457
  else pack();
package/bin/profile.mjs CHANGED
@@ -127,7 +127,8 @@ async function wizard() {
127
127
  let pipedLines = [];
128
128
  if (piped) {
129
129
  try {
130
- pipedLines = fs.readFileSync(0, 'utf8').split('\n').map((l) => l.trim());
130
+ const raw = fs.readFileSync(0, 'utf8');
131
+ pipedLines = raw.trim() ? raw.split('\n').map((l) => l.trim()) : [];
131
132
  } catch { /* empty stdin -> quit below */ }
132
133
  }
133
134
  let pipedPos = 0;
@@ -39,14 +39,14 @@ const DESCRIPTIONS = {
39
39
  audit: 'pre-submission hard-fail gate',
40
40
  render: 'fallback markdown -> PDF emitter (no document tooling needed)',
41
41
  package: 'verify final docs (re-extract + re-audit) -> pack/ + checklist + zip',
42
- 'bid-pack': 'one-shot orchestrator: start | apply | pack + the consolidated questionnaire',
42
+ 'bid-pack': 'one-shot orchestrator: start | ask | apply | pack + the consolidated questionnaire',
43
43
  profile: 'consultant profile wizard (.tor-to-proposal/profile.json in the current folder)',
44
44
  init: 'alias for profile — one-time setup wizard',
45
45
  };
46
46
 
47
47
  const [cmd, ...rest] = process.argv.slice(2);
48
48
  if (!cmd || cmd === 'help' || cmd === '--help') {
49
- process.stdout.write(`tor-to-proposal — the bid tool that refuses to lie.\n\nUsage: tor-to-proposal <command> [options]\n\nQuick start (plug and play):\n tor-to-proposal init one-time profile + setup wizard\n tor-to-proposal bid-pack start --tor tor.pdf --cv cv.txt --dir mybid/\n tor-to-proposal bid-pack apply --answers answers.json --dir mybid/\n tor-to-proposal bid-pack pack --dir mybid/ --fallback-pdf\n\nCommands:\n${Object.keys(TOOLS).map((k) => ` ${k.padEnd(19)} ${DESCRIPTIONS[k]}`).join('\n')}\n\nPer-command help: tor-to-proposal <command> --help\nAgent workflow: see SKILL.md\n`);
49
+ process.stdout.write(`tor-to-proposal — the bid tool that refuses to lie.\n\nUsage: tor-to-proposal <command> [options]\n\nQuick start (plug and play):\n tor-to-proposal init one-time profile + setup wizard\n tor-to-proposal bid-pack start --tor tor.pdf --cv cv.txt --dir mybid/\n tor-to-proposal bid-pack ask --dir mybid/ answer open questions interactively\n tor-to-proposal bid-pack pack --dir mybid/ --fallback-pdf\n\nCommands:\n${Object.keys(TOOLS).map((k) => ` ${k.padEnd(19)} ${DESCRIPTIONS[k]}`).join('\n')}\n\nPer-command help: tor-to-proposal <command> --help\nAgent workflow: see SKILL.md\n`);
50
50
  process.exit(cmd ? 0 : 1);
51
51
  }
52
52
  if (!TOOLS[cmd]) {
@@ -193,6 +193,82 @@ you will ever own.
193
193
 
194
194
  ---
195
195
 
196
+ ## 6b. How to answer the questions (`out/questions.md` → `answers.json`)
197
+
198
+ After `bid-pack start`, the tool writes **`out/questions.md`** — a numbered
199
+ table of everything it cannot decide for you (your price confirmation, effort
200
+ days, availability date, and every `[FILL]` placeholder it found).
201
+
202
+ **You do not edit questions.md itself.** You answer in one of three ways:
203
+
204
+ ### Way 1 (easiest, no editor needed): the terminal wizard
205
+
206
+ ```bash
207
+ npx tor-to-proposal bid-pack ask --dir .
208
+ ```
209
+
210
+ It asks every open question **one at a time, right in the Terminal** — exactly
211
+ like the `init` wizard. Press Enter to accept a suggested value, type `back`
212
+ to redo the previous question, `quit` to stop. At the end it saves your
213
+ answers and applies them automatically. Run it again anytime; it only asks
214
+ what is still open.
215
+
216
+ ### Way 2: reply to your AI assistant
217
+ questions.md ends with the exact instruction. Just type your numbered answers
218
+ in the chat, e.g.:
219
+
220
+ ```
221
+ 1. yes
222
+ 2. 25 person-days
223
+ 3. 1 November 2026
224
+ 4. 90 days from submission
225
+ ```
226
+
227
+ The assistant writes them into `answers.json` for you and runs
228
+ `bid-pack apply`.
229
+
230
+ ### Way 3 (by hand): write answers.json yourself
231
+ Create a file called `answers.json` **in your bid folder** (next to
232
+ `tor.pdf`). It is a simple JSON object: **question ID → your answer**.
233
+ Copy the IDs exactly from `out/questions.json` or the **bold** codes in
234
+ questions.md. A complete sample:
235
+
236
+ ```json
237
+ {
238
+ "Q-PRICING-CONFIRM": "yes",
239
+ "Q-EFFORT-DAYS": "25 person-days",
240
+ "Q-AVAILABILITY": "1 November 2026",
241
+ "Q-VALIDITY": "90 days from submission",
242
+ "Q-FILL-TAXES": "Prices exclude VAT; consultant is registered with FBR.",
243
+ "Q-FILL-REIMBURSABLES": "Travel and interpretation costs are reimbursable at cost against receipts.",
244
+ "Q-CVE-1": "Led three UNDP M&E assignments (2022–2025) as team lead.",
245
+ "Q-CLAR-1": "Requested the final evaluation grid by email on 3 Oct 2026."
246
+ }
247
+ ```
248
+
249
+ What the special IDs mean:
250
+
251
+ | ID pattern | Meaning | What to answer |
252
+ | --- | --- | --- |
253
+ | `Q-PRICING-CONFIRM` | Your saved profile rate offered for this bid | `yes` to use it, or new numbers: `"base 420 loading 0.25 contingency 0.1"` |
254
+ | `Q-EFFORT-DAYS` | Person-days you will quote | e.g. `"25 person-days"` |
255
+ | `Q-AVAILABILITY` | When you can start | any honest date |
256
+ | `Q-VALIDITY` | How long your offer stays valid | e.g. `"90 days from submission"` |
257
+ | `Q-FILL-...` | A `[FILL]` box in a draft document | the real fact that belongs there |
258
+ | `Q-CVE-n` | A CV line needs proof (or removal) | the true fact, or `"delete"` |
259
+ | `Q-CLAR-n` | Something to ask the CLIENT | what you asked them and when |
260
+
261
+ Then run:
262
+
263
+ ```bash
264
+ npx tor-to-proposal bid-pack apply --answers answers.json --dir .
265
+ ```
266
+
267
+ `apply` re-prints the questionnaire — repeat until it says **0 open
268
+ questions**, then run `bid-pack pack`.
269
+
270
+ ---
271
+
196
272
  ## 7. If something goes wrong
197
273
 
198
274
  | What you see | What it means | What to do |
@@ -222,6 +298,9 @@ you will ever own.
222
298
  - **Master CV** — your full, real CV. The tailored one is derived from it and
223
299
  verified against it.
224
300
  - **`[FILL]`** — a yellow flag meaning "a real fact goes here — yours".
301
+ - **`out/preview/`** — readable PDF copies of the bid screen, the questions
302
+ and the CV reports, regenerated after every `start` and `apply`. Double-click
303
+ to read them; the `.md` originals are the working files the tool edits.
225
304
  - **Floor rate** — the lowest daily rate that still pays your bills. Below it,
226
305
  you're paying the client.
227
306
  - **MCP / skill / plugin** — three plumbing formats for connecting the tool to
package/mcp/server.mjs CHANGED
@@ -322,7 +322,7 @@ function handle(msg) {
322
322
  return {
323
323
  protocolVersion: typeof params?.protocolVersion === 'string' ? params.protocolVersion : '2024-11-05',
324
324
  capabilities: { tools: {} },
325
- serverInfo: { name: 'tor-to-proposal', version: '2.0.1' },
325
+ serverInfo: { name: 'tor-to-proposal', version: '2.1.0' },
326
326
  };
327
327
  }
328
328
  if (typeof method === 'string' && method.startsWith('notifications/')) return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tor-to-proposal",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "The bid tool that refuses to lie. ToR + CV in -> verified bid pack out: cover letter, tailored CV, technical response, financial proposal, submission checklist. Deterministic CLI + agent skill for UN / NGO / government consultants.",
5
5
  "type": "module",
6
6
  "bin": {