tor-to-proposal 2.1.0 → 2.1.4
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/bin/bid-pack.mjs +49 -4
- package/mcp/server.mjs +1 -1
- package/package.json +1 -1
package/bin/bid-pack.mjs
CHANGED
|
@@ -139,6 +139,29 @@ function buildQuestions() {
|
|
|
139
139
|
return { generatedAt: new Date().toISOString(), dir: dir, openCount: items.length, items };
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// Per-question "how to answer" hint, shown in the wizard prompt and the
|
|
143
|
+
// questions table so users know the accepted format before replying.
|
|
144
|
+
function answerFormat(item) {
|
|
145
|
+
switch (item.id) {
|
|
146
|
+
case 'Q-PRICING-CONFIRM': return 'type yes to use the saved rates — or new numbers like: base 420 loading 0.25 contingency 0.1';
|
|
147
|
+
case 'Q-PRICING-BASE': return 'a number, e.g. 400';
|
|
148
|
+
case 'Q-PRICING-LOADING': return 'a decimal, e.g. 0.25 (= 25%)';
|
|
149
|
+
case 'Q-PRICING-CONTINGENCY': return 'a decimal, e.g. 0.10 (= 10%)';
|
|
150
|
+
case 'Q-PRICING-CURRENCY': return 'a currency code, e.g. USD';
|
|
151
|
+
case 'Q-EFFORT-DAYS': return 'a number of person-days, e.g. 25';
|
|
152
|
+
case 'Q-AVAILABILITY': return 'a date, e.g. 1 November 2026';
|
|
153
|
+
case 'Q-VALIDITY': return 'a phrase, e.g. 90 days from submission';
|
|
154
|
+
case 'Q-FILL-CONSULTANT': return 'the consultant name as it should appear in the document';
|
|
155
|
+
case 'Q-FILL-REIMBURSABLES': return 'a sentence about which costs are reimbursable';
|
|
156
|
+
case 'Q-FILL-TAXES': return 'a sentence about your tax status';
|
|
157
|
+
}
|
|
158
|
+
if (item.id.startsWith('Q-CVE-')) return 'the exact true wording that supports the line — or DELETE to remove the line';
|
|
159
|
+
if (item.type === 'number') return `a number${item.unit ? ` in ${item.unit}` : ''}`;
|
|
160
|
+
if (item.type === 'date') return 'a date, e.g. 1 November 2026';
|
|
161
|
+
if (item.type === 'confirm') return 'yes or no';
|
|
162
|
+
return 'a short sentence';
|
|
163
|
+
}
|
|
164
|
+
|
|
142
165
|
function writeQuestions(q) {
|
|
143
166
|
fs.writeFileSync(path.join(outDir, 'questions.json'), JSON.stringify(q, null, 2));
|
|
144
167
|
const yours = q.items.filter((i) => i.type !== 'client');
|
|
@@ -147,7 +170,7 @@ function writeQuestions(q) {
|
|
|
147
170
|
const intro = `Reply with the numbered answers; your assistant writes them to answers.json and runs:\n\n tor-to-proposal bid-pack apply --answers answers.json --dir ${dir}\n`;
|
|
148
171
|
const md =
|
|
149
172
|
`# Bid questions — answer these in one go\n\n` +
|
|
150
|
-
(yours.length ? intro + '\n' + mdTable(['#', 'Question', 'Why it is asked'], yours.map((i) => [++n, `**${i.id}** — ${i.question}${i.default !== undefined && i.default !== null ? ` (default: ${i.default})` : ''}`, i.why || ''])) + '\n' : 'None — everything mechanical is resolved.\n') +
|
|
173
|
+
(yours.length ? intro + '\n' + mdTable(['#', 'Question', 'How to answer', 'Why it is asked'], yours.map((i) => [++n, `**${i.id}** — ${i.question}${i.default !== undefined && i.default !== null ? ` (default: ${i.default})` : ''}`, answerFormat(i), i.why || ''])) + '\n' : 'None — everything mechanical is resolved.\n') +
|
|
151
174
|
(clients.length ? `\n## To send to the CLIENT (clarifications, before the cutoff)\n${clients.map((i) => `- ${i.question}`).join('\n')}\n` : '');
|
|
152
175
|
writeOut(path.join(outDir, 'questions.md'), md);
|
|
153
176
|
}
|
|
@@ -327,11 +350,17 @@ function apply() {
|
|
|
327
350
|
if (pricing.loading !== null && pricing.loading !== undefined) argv.push('--loading', String(pricing.loading));
|
|
328
351
|
if (pricing.contingency !== null && pricing.contingency !== undefined) argv.push('--contingency', String(pricing.contingency));
|
|
329
352
|
const extractPath = inDir('tor-extract.json');
|
|
330
|
-
|
|
353
|
+
// users answer effort in natural language ("25", "25 person-days", "about 25 days") —
|
|
354
|
+
// pricing-model needs a bare number, so coerce here instead of failing pricing
|
|
355
|
+
const effortRaw = pricing.effort ?? (extractPath && JSON.parse(fs.readFileSync(extractPath, 'utf8')).effortPersonDays?.value);
|
|
356
|
+
const effortMatch = effortRaw === null || effortRaw === undefined ? null : String(effortRaw).replace(/,/g, '').match(/-?\d+(\.\d+)?/);
|
|
357
|
+
const effort = effortMatch ? Number(effortMatch[0]) : null;
|
|
331
358
|
if (effort !== null && effort !== undefined) argv.push('--effort', String(effort));
|
|
332
359
|
const r = run('pricing-model.mjs', argv, { label: 'pricing-model' });
|
|
333
360
|
pricingRan = r !== null;
|
|
334
361
|
process.stderr.write(r === null ? 'pricing NOT written — check the values; the pricing question stays open.\n' : `pricing written from user-confirmed numbers -> ${outDir}/pricing.json\n`);
|
|
362
|
+
} else if (answers['Q-PRICING-CONFIRM'] !== undefined) {
|
|
363
|
+
process.stderr.write('Q-PRICING-CONFIRM: no usable pricing in that answer — reply "yes" to use your saved rates, or new numbers like "base 420 loading 0.25". The pricing question stays open.\n');
|
|
335
364
|
}
|
|
336
365
|
|
|
337
366
|
// side effect: (re)render the financial proposal when its inputs are ready
|
|
@@ -351,6 +380,8 @@ function apply() {
|
|
|
351
380
|
fresh.items = fresh.items.filter((i) => i.answer === null);
|
|
352
381
|
fresh.openCount = fresh.items.length;
|
|
353
382
|
writeQuestions(fresh);
|
|
383
|
+
const stillYours = fresh.items.filter((i) => i.type !== 'client');
|
|
384
|
+
const stillClient = fresh.items.filter((i) => i.type === 'client');
|
|
354
385
|
const state = readState();
|
|
355
386
|
state.steps.questions = { status: 'done', open: fresh.openCount };
|
|
356
387
|
state.steps.pricing = { status: inDir('pricing.json') ? 'done' : 'awaiting-user-numbers' };
|
|
@@ -359,7 +390,13 @@ function apply() {
|
|
|
359
390
|
|
|
360
391
|
process.stdout.write(
|
|
361
392
|
`# Apply: ${applied} answer(s) recorded, ${fresh.openCount} still open\n\n` +
|
|
362
|
-
(
|
|
393
|
+
(stillYours.length
|
|
394
|
+
? `## Still open — answer these (bid-pack ask)\n${mdTable(['#', 'Question'], stillYours.map((i, ix) => [ix + 1, `**${i.id}** — ${i.question}`]))}\n`
|
|
395
|
+
: '') +
|
|
396
|
+
(stillClient.length
|
|
397
|
+
? `## Reminders — ACTIONS for you, not answers\n(These describe things to DO outside the tool: contact the client, verify a fact. They never close by typing an answer here.)\n${mdTable(['#', 'Action'], stillClient.map((i, ix) => [ix + 1, `**${i.id}** — ${i.question}`]))}\n`
|
|
398
|
+
: '') +
|
|
399
|
+
(!fresh.openCount ? `All questions resolved. Draft/generate, then: bid-pack pack --dir ${dir}\n` : '') +
|
|
363
400
|
(fills.length ? `\nFill answers recorded in out/fill-answers.json — apply them to the drafts, re-run the lints, and the questions disappear once the [FILL]s are gone.\n` : '') +
|
|
364
401
|
(evidence.length ? `\nCV evidence entries: ${evidence.length} (out/cv-evidence.json) — cv-tailor lint accepts these as user-input anchors.\n` : '')
|
|
365
402
|
);
|
|
@@ -410,12 +447,16 @@ async function askWizard() {
|
|
|
410
447
|
);
|
|
411
448
|
|
|
412
449
|
const answers = {};
|
|
450
|
+
// a pricing answer is only usable if it is "yes" or contains a base number —
|
|
451
|
+
// anything else would silently skip pricing and re-open the question later,
|
|
452
|
+
// so the wizard rejects it on the spot and re-asks
|
|
453
|
+
const pricingAnswerUsable = (a) => /^yes$/i.test(a) || /\bbase\b\s*[\d]/i.test(a);
|
|
413
454
|
let i = 0;
|
|
414
455
|
while (i < yours.length) {
|
|
415
456
|
const item = yours[i];
|
|
416
457
|
const cur = item.default !== undefined && item.default !== null ? String(item.default) : '';
|
|
417
458
|
const tail = item.unit ? ` (${item.unit})` : '';
|
|
418
|
-
const a = await askLine(`(${i + 1}/${yours.length}) ${item.question}${tail}${cur ? ` [${cur}]` : ''}\n> `);
|
|
459
|
+
const a = await askLine(`(${i + 1}/${yours.length}) ${item.question}${tail}${cur ? ` [${cur}]` : ''}\n How to answer: ${answerFormat(item)}\n> `);
|
|
419
460
|
if (a === 'EOF' || a.toLowerCase() === 'quit') {
|
|
420
461
|
rl.close();
|
|
421
462
|
process.stdout.write('\nCancelled — nothing was saved. (Your documents are untouched; run bid-pack ask again anytime.)\n');
|
|
@@ -432,6 +473,10 @@ async function askWizard() {
|
|
|
432
473
|
process.stdout.write(' This one needs an answer from you (no suggested default). Type it, or "quit" to stop.\n');
|
|
433
474
|
continue;
|
|
434
475
|
}
|
|
476
|
+
if (item.id === 'Q-PRICING-CONFIRM' && a !== '' && !pricingAnswerUsable(a)) {
|
|
477
|
+
process.stdout.write(' I can\'t price from that alone. Either type "yes" to use the saved rates shown above, or give new numbers like: base 420 loading 0.25 contingency 0.1\n');
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
435
480
|
answers[item.id] = a === '' ? item.default : a;
|
|
436
481
|
i += 1;
|
|
437
482
|
}
|
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.1.
|
|
325
|
+
serverInfo: { name: 'tor-to-proposal', version: '2.1.4' },
|
|
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.1.
|
|
3
|
+
"version": "2.1.4",
|
|
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": {
|