tor-to-proposal 2.0.0 → 2.0.1
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/README.md +3 -1
- package/bin/bid-pack.mjs +2 -3
- package/bin/profile.mjs +125 -48
- package/bin/tor-to-proposal.mjs +6 -2
- package/docs/TECHNICAL-GUIDE.md +2 -2
- package/mcp/server.mjs +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,9 @@ npx tor-to-proposal init
|
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
That sets up your consultant profile (name, credentials, rates, master CV) at
|
|
31
|
-
|
|
31
|
+
`.tor-to-proposal/profile.json` in the **current folder** (the legacy
|
|
32
|
+
`~/.tor-to-proposal/profile.json` is still read if no local profile exists).
|
|
33
|
+
Then use it with any AI assistant (Claude
|
|
32
34
|
Desktop / Claude Code / Codex / ChatGPT) via the MCP server at
|
|
33
35
|
`mcp/server.mjs`, or install the agent skill:
|
|
34
36
|
|
package/bin/bid-pack.mjs
CHANGED
|
@@ -12,13 +12,12 @@
|
|
|
12
12
|
// GATES PRESERVED: pricing never runs without user numbers; GAP terms never
|
|
13
13
|
// claimed; audit gates the final bytes. The orchestrator only automates plumbing.
|
|
14
14
|
import fs from 'node:fs';
|
|
15
|
-
import os from 'node:os';
|
|
16
15
|
import path from 'node:path';
|
|
17
16
|
import crypto from 'node:crypto';
|
|
18
17
|
import { spawnSync } from 'node:child_process';
|
|
19
18
|
import { fileURLToPath } from 'node:url';
|
|
20
19
|
import { fail, parseArgs, readText, writeOut, helpText, mdTable } from './lib.mjs';
|
|
21
|
-
import { loadProfile } from './profile.mjs';
|
|
20
|
+
import { loadProfile, profilePath } from './profile.mjs';
|
|
22
21
|
|
|
23
22
|
const HELP = helpText('bid-pack', [
|
|
24
23
|
'start --tor <file.(pdf|docx|txt)> [--cv <file>] [--dir bid/] [--profile] [--no-cv-tailor]',
|
|
@@ -203,7 +202,7 @@ function start() {
|
|
|
203
202
|
run('cv-gap.mjs', ['--tor', path.join(outDir, 'tor.txt'), '--cv', cvTxt, '--out', path.join(outDir, 'cv-gap-report.md')], { label: 'cv-gap' });
|
|
204
203
|
state.steps.cvGap = { status: 'done' };
|
|
205
204
|
if (!inDir('cv-tailored.md') && !args['no-cv-tailor']) {
|
|
206
|
-
const profFlag = (args.profile === undefined || args.profile === true) &&
|
|
205
|
+
const profFlag = (args.profile === undefined || args.profile === true) && loadProfile() ? ['--profile', profilePath()] : [];
|
|
207
206
|
run('cv-tailor.mjs', ['build', '--cv', cvTxt, '--extract', path.join(outDir, 'tor-extract.json'), ...profFlag, '--gap', path.join(outDir, 'cv-gap-report.md'), '--out', path.join(outDir, 'cv-tailored.md'), '--report', path.join(outDir, 'cv-tailor-report.md')], { label: 'cv-tailor build' });
|
|
208
207
|
}
|
|
209
208
|
state.steps.cvTailored = { status: inDir('cv-tailored.md') ? 'done' : 'skipped' };
|
package/bin/profile.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// profile: the one-time consultant profile ->
|
|
2
|
+
// profile: the one-time consultant profile -> .tor-to-proposal/profile.json
|
|
3
|
+
// (in the CURRENT folder, so users can see and back it up like any project file;
|
|
4
|
+
// the legacy ~/.tor-to-proposal/profile.json is still read as a fallback).
|
|
3
5
|
// Every value here is USER-SUPPLIED. The profile is the canonical "user input"
|
|
4
6
|
// trace source downstream tools cite; nothing in it is ever invented or defaulted
|
|
5
7
|
// silently. Plug-and-play: `npx tor-to-proposal init` runs the wizard once, and
|
|
@@ -11,9 +13,9 @@ import readline from 'node:readline/promises';
|
|
|
11
13
|
import { fail, parseArgs, helpText } from './lib.mjs';
|
|
12
14
|
|
|
13
15
|
const HELP = helpText('profile', [
|
|
14
|
-
'One-time consultant profile, stored at
|
|
16
|
+
'One-time consultant profile, stored at .tor-to-proposal/profile.json in the CURRENT folder.',
|
|
15
17
|
'',
|
|
16
|
-
' init interactive wizard (terminal prompts; blank = keep current)',
|
|
18
|
+
' init interactive wizard (terminal prompts; blank = default/keep current)',
|
|
17
19
|
' set key=value ... set fields (dotted keys). Values parse as JSON when valid:',
|
|
18
20
|
' profile set identity.name="Ayesha Khan" rates.defaults.base=400 cv.masterPath=~/cv/master.md',
|
|
19
21
|
' get [key] print the whole profile (or one dotted field) as JSON',
|
|
@@ -27,7 +29,12 @@ const HELP = helpText('profile', [
|
|
|
27
29
|
' cv.masterPath # your master CV',
|
|
28
30
|
]);
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
// Profile lives in the current folder (cwd-first); the legacy home location is
|
|
33
|
+
// only read as a fallback so profiles created by older versions keep working.
|
|
34
|
+
export function profilePath() {
|
|
35
|
+
return path.resolve('.tor-to-proposal', 'profile.json');
|
|
36
|
+
}
|
|
37
|
+
const LEGACY_FILE = path.join(os.homedir(), '.tor-to-proposal', 'profile.json');
|
|
31
38
|
|
|
32
39
|
const NUMERIC = {
|
|
33
40
|
'rates.floor.annualIncome': { min: 0, max: 1e9, label: 'annual income need' },
|
|
@@ -39,14 +46,18 @@ const NUMERIC = {
|
|
|
39
46
|
};
|
|
40
47
|
const BASIS = ['hour', 'day', 'week', 'month'];
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
function readProfileAt(file) {
|
|
43
50
|
try {
|
|
44
|
-
return JSON.parse(fs.readFileSync(
|
|
51
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
45
52
|
} catch {
|
|
46
53
|
return null;
|
|
47
54
|
}
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
export function loadProfile() {
|
|
58
|
+
return readProfileAt(profilePath()) || readProfileAt(LEGACY_FILE);
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
function emptyProfile() {
|
|
51
62
|
const now = new Date().toISOString();
|
|
52
63
|
return {
|
|
@@ -100,56 +111,119 @@ function coerceAndValidate(dotted, raw) {
|
|
|
100
111
|
|
|
101
112
|
function save(profile) {
|
|
102
113
|
profile.meta = { ...(profile.meta || {}), updatedAt: new Date().toISOString() };
|
|
103
|
-
|
|
104
|
-
fs.
|
|
105
|
-
|
|
114
|
+
const file = profilePath();
|
|
115
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
116
|
+
fs.writeFileSync(file, JSON.stringify(profile, null, 2));
|
|
117
|
+
process.stderr.write(`written: ${file}\n`);
|
|
106
118
|
}
|
|
107
119
|
|
|
108
120
|
// ---------- interactive wizard ----------
|
|
109
121
|
async function wizard() {
|
|
110
122
|
const current = loadProfile() || emptyProfile();
|
|
123
|
+
|
|
124
|
+
// Answers come from a real terminal when interactive; when stdin is piped
|
|
125
|
+
// (automation, tests), all lines are read up front and consumed in order.
|
|
126
|
+
const piped = !process.stdin.isTTY;
|
|
127
|
+
let pipedLines = [];
|
|
128
|
+
if (piped) {
|
|
129
|
+
try {
|
|
130
|
+
pipedLines = fs.readFileSync(0, 'utf8').split('\n').map((l) => l.trim());
|
|
131
|
+
} catch { /* empty stdin -> quit below */ }
|
|
132
|
+
}
|
|
133
|
+
let pipedPos = 0;
|
|
134
|
+
const nextLine = async () => (piped ? (pipedPos < pipedLines.length ? pipedLines[pipedPos++] : 'EOF') : '');
|
|
135
|
+
|
|
111
136
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const cur = existing === null || existing === undefined || (Array.isArray(existing) && !existing.length) ? '' : String(Array.isArray(existing) ? existing.join(', ') : existing);
|
|
115
|
-
const a = (await rl.question(`${label}${tail} [${cur}]: `)).trim();
|
|
116
|
-
return a === '' ? existing : a;
|
|
117
|
-
};
|
|
137
|
+
|
|
138
|
+
// Each question: [get set on the profile, label, hint, default, toValue]
|
|
118
139
|
const id = current.identity;
|
|
119
|
-
id.name = await ask('Full name (exactly as on your CV)', id.name);
|
|
120
|
-
id.credentials = await ask('Credentials after your name (e.g. "PhD, PMP")', id.credentials);
|
|
121
|
-
id.title = await ask('Professional title (e.g. "M&E Consultant")', id.title);
|
|
122
|
-
id.email = await ask('Email', id.email);
|
|
123
|
-
id.phone = await ask('Phone (international format)', id.phone);
|
|
124
|
-
id.location = await ask('City, country', id.location);
|
|
125
|
-
id.nationality = await ask('Nationality (some ToRs require it)', id.nationality);
|
|
126
|
-
id.languages = await ask('Working languages (comma-separated)', id.languages);
|
|
127
|
-
const cv = await ask('Path to your master CV file', current.cv.masterPath);
|
|
128
|
-
if (cv !== current.cv.masterPath) {
|
|
129
|
-
const p = String(cv).replace(/^~(?=\/|$)/, os.homedir());
|
|
130
|
-
if (!fs.existsSync(p)) process.stderr.write(` ! file not found (${p}) — stored anyway; fix with: profile set cv.masterPath=...\n`);
|
|
131
|
-
current.cv.masterPath = p;
|
|
132
|
-
}
|
|
133
140
|
const fl = current.rates.floor;
|
|
134
|
-
const asNum = (v) => (v === null || v === undefined || v === '' ? null : Number(v));
|
|
135
|
-
let a = await ask('Minimum annual income you need (your floor math)', fl.annualIncome, 'e.g. 60000');
|
|
136
|
-
fl.annualIncome = a === fl.annualIncome ? fl.annualIncome : asNum(a);
|
|
137
|
-
a = await ask('Realistic billable days per year', fl.billableDays, 'e.g. 180');
|
|
138
|
-
fl.billableDays = a === fl.billableDays ? fl.billableDays : asNum(a);
|
|
139
|
-
a = await ask('Cost loading (overheads on top of your net)', fl.costLoading, 'e.g. 0.25');
|
|
140
|
-
fl.costLoading = a === fl.costLoading ? fl.costLoading : asNum(a);
|
|
141
141
|
const d = current.rates.defaults;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
142
|
+
const expandTilde = (v) => String(v).replace(/^~(?=\/|$)/, os.homedir());
|
|
143
|
+
const toNum = (v) => (v === null || v === undefined || v === '' ? null : Number(v));
|
|
144
|
+
const questions = [
|
|
145
|
+
{ label: 'Full name (exactly as on your CV)', get: () => id.name, set: (v) => { id.name = v; } },
|
|
146
|
+
{ label: 'Credentials after your name (e.g. "PhD, PMP")', get: () => id.credentials, set: (v) => { id.credentials = v; } },
|
|
147
|
+
{ label: 'Professional title (e.g. "M&E Consultant")', get: () => id.title, set: (v) => { id.title = v; } },
|
|
148
|
+
{ label: 'Email', get: () => id.email, set: (v) => { id.email = v; } },
|
|
149
|
+
{ label: 'Phone (international format)', get: () => id.phone, set: (v) => { id.phone = v; } },
|
|
150
|
+
{ label: 'City, country', get: () => id.location, set: (v) => { id.location = v; } },
|
|
151
|
+
{ label: 'Nationality (some ToRs require it)', get: () => id.nationality, set: (v) => { id.nationality = v; } },
|
|
152
|
+
{ label: 'Working languages (comma-separated)', get: () => id.languages, set: (v) => { id.languages = v; } },
|
|
153
|
+
{
|
|
154
|
+
label: 'Path to your master CV file', get: () => current.cv.masterPath,
|
|
155
|
+
set: (v) => {
|
|
156
|
+
if (v === null || v === undefined || v === '') { current.cv.masterPath = current.cv.masterPath ?? null; return; }
|
|
157
|
+
const p = expandTilde(v);
|
|
158
|
+
if (!fs.existsSync(p)) process.stderr.write(` ! file not found (${p}) — stored anyway; fix with: profile set cv.masterPath=...\n`);
|
|
159
|
+
current.cv.masterPath = p;
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{ label: 'Minimum annual income you need (your floor math)', hint: 'e.g. 60000', get: () => fl.annualIncome, set: (v) => { fl.annualIncome = toNum(v); } },
|
|
163
|
+
{ label: 'Realistic billable days per year', hint: 'e.g. 180', get: () => fl.billableDays, set: (v) => { fl.billableDays = toNum(v); } },
|
|
164
|
+
{ label: 'Cost loading (overheads on top of your net)', hint: 'e.g. 0.25', get: () => fl.costLoading, set: (v) => { fl.costLoading = toNum(v); } },
|
|
165
|
+
{ label: 'Default currency', get: () => d.currency || 'USD', set: (v) => { d.currency = String(v).toUpperCase(); } },
|
|
166
|
+
{ label: 'Typical base rate (optional — you confirm it per bid)', hint: 'e.g. 400', get: () => d.base, set: (v) => { d.base = toNum(v); } },
|
|
167
|
+
{ label: 'Typical loading', hint: 'e.g. 0.25', get: () => d.loading, set: (v) => { d.loading = toNum(v); } },
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
process.stdout.write(
|
|
171
|
+
`Consultant profile wizard — ${questions.length} questions, one at a time.\n` +
|
|
172
|
+
'Press Enter to accept the [suggested] value. Type "back" to redo the previous question, "quit" to cancel.\n\n'
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const fmt = (v) => (v === null || v === undefined || (Array.isArray(v) && !v.length)) ? '' : String(Array.isArray(v) ? v.join(', ') : v);
|
|
176
|
+
const askLine = async (prompt) => {
|
|
177
|
+
if (piped) return nextLine();
|
|
178
|
+
try {
|
|
179
|
+
return (await rl.question(prompt)).trim();
|
|
180
|
+
} catch (err) {
|
|
181
|
+
if (err && (err.code === 'ABORT_ERR' || err.name === 'AbortError')) return 'EOF'; // Ctrl+D
|
|
182
|
+
throw err;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
let i = 0;
|
|
186
|
+
while (i < questions.length) {
|
|
187
|
+
const q = questions[i];
|
|
188
|
+
const cur = fmt(q.get());
|
|
189
|
+
const tail = q.hint ? ` (${q.hint})` : '';
|
|
190
|
+
const a = await askLine(`(${i + 1}/${questions.length}) ${q.label}${tail} [${cur}]: `);
|
|
191
|
+
if (a === 'EOF' || a.toLowerCase() === 'quit') {
|
|
192
|
+
rl.close();
|
|
193
|
+
process.stdout.write('\nCancelled — nothing was saved.\n');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (a.toLowerCase() === 'back') {
|
|
197
|
+
if (i === 0) { process.stdout.write(' (already at the first question)\n'); continue; }
|
|
198
|
+
i -= 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
// blank = keep the current/suggested value shown in brackets
|
|
202
|
+
q.set(a === '' ? q.get() : a);
|
|
203
|
+
i += 1;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Summary + confirm before anything is written
|
|
207
|
+
const fields = [
|
|
208
|
+
['name', id.name], ['credentials', id.credentials], ['title', id.title], ['email', id.email],
|
|
209
|
+
['phone', id.phone], ['location', id.location], ['nationality', id.nationality], ['languages', id.languages],
|
|
210
|
+
['master CV', current.cv.masterPath],
|
|
211
|
+
['min annual income', fl.annualIncome], ['billable days/year', fl.billableDays], ['cost loading', fl.costLoading],
|
|
212
|
+
['currency', d.currency], ['typical base rate', d.base], ['typical loading', d.loading],
|
|
213
|
+
];
|
|
214
|
+
process.stdout.write('\n--- Your answers ---\n');
|
|
215
|
+
for (const [k, v] of fields) process.stdout.write(` ${k.padEnd(20)} ${fmt(v) || '(empty)'}\n`);
|
|
216
|
+
const confirm = await askLine('\nSave this profile? (Y/n): ');
|
|
148
217
|
rl.close();
|
|
218
|
+
if (confirm === 'EOF' || confirm === 'n' || confirm === 'no') {
|
|
219
|
+
process.stdout.write('Cancelled — nothing was saved.\n');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
149
222
|
save(current);
|
|
150
223
|
process.stdout.write(
|
|
151
|
-
`\nProfile saved: ${
|
|
152
|
-
|
|
224
|
+
`\nProfile saved: ${profilePath()}\n(it lives in the CURRENT folder — open it with: open .tor-to-proposal)\n\n` +
|
|
225
|
+
'Next: drop a ToR + your CV into a folder and tell your AI assistant:\n "Run a bid pack on <folder>"\n' +
|
|
226
|
+
'Your saved numbers will be offered for one-keystroke confirmation — never used without it.\n'
|
|
153
227
|
);
|
|
154
228
|
}
|
|
155
229
|
|
|
@@ -182,17 +256,20 @@ if (cmd === 'init') {
|
|
|
182
256
|
save(profile);
|
|
183
257
|
} else if (cmd === 'get') {
|
|
184
258
|
const profile = loadProfile();
|
|
185
|
-
if (!profile) fail(`no profile at ${
|
|
259
|
+
if (!profile) fail(`no profile at ${profilePath()} — run: tor-to-proposal init`, 2);
|
|
186
260
|
if (args._[1]) {
|
|
187
261
|
const v = getDeep(profile, args._[1]);
|
|
188
262
|
if (v === undefined) fail(`unknown key: ${args._[1]}`, 2);
|
|
189
263
|
process.stdout.write(JSON.stringify(v, null, 2) + '\n');
|
|
190
264
|
} else process.stdout.write(JSON.stringify(profile, null, 2) + '\n');
|
|
191
265
|
} else if (cmd === 'path') {
|
|
192
|
-
process.stdout.write(
|
|
266
|
+
process.stdout.write(profilePath() + '\n');
|
|
193
267
|
} else if (cmd === 'erase') {
|
|
194
|
-
|
|
195
|
-
|
|
268
|
+
let erased = false;
|
|
269
|
+
for (const file of [profilePath(), LEGACY_FILE]) {
|
|
270
|
+
if (fs.existsSync(file)) { fs.rmSync(file); process.stderr.write(`erased: ${file}\n`); erased = true; }
|
|
271
|
+
}
|
|
272
|
+
if (!erased) process.stderr.write('no profile to erase\n');
|
|
196
273
|
} else {
|
|
197
274
|
fail(`unknown command: ${cmd} (init | set | get | path | erase)`, 2);
|
|
198
275
|
}
|
package/bin/tor-to-proposal.mjs
CHANGED
|
@@ -40,7 +40,7 @@ const DESCRIPTIONS = {
|
|
|
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
42
|
'bid-pack': 'one-shot orchestrator: start | apply | pack + the consolidated questionnaire',
|
|
43
|
-
profile: 'consultant profile wizard (
|
|
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
|
|
|
@@ -59,5 +59,9 @@ if (!TOOLS[cmd]) {
|
|
|
59
59
|
process.exit(1);
|
|
60
60
|
}
|
|
61
61
|
const script = path.join(path.dirname(fileURLToPath(import.meta.url)), TOOLS[cmd]);
|
|
62
|
-
|
|
62
|
+
// `init` is an alias for `profile init` — without the subcommand profile.mjs
|
|
63
|
+
// would only print its help (this is why plain `tor-to-proposal init` never
|
|
64
|
+
// started the wizard before).
|
|
65
|
+
const sub = cmd === 'init' ? ['init', ...rest] : rest;
|
|
66
|
+
const res = spawnSync(process.execPath, [script, ...sub], { stdio: 'inherit' });
|
|
63
67
|
process.exit(res.status ?? 1);
|
package/docs/TECHNICAL-GUIDE.md
CHANGED
|
@@ -57,7 +57,7 @@ formula overwrite) · **3** dependencies missing.
|
|
|
57
57
|
| `bid-pack start` | `--tor file [--cv file] [--dir bid/] [--no-cv-tailor]` | runs pdf-extract → extract → cv-gap → cv-tailor; `out/questions.{json,md}` (consolidated questionnaire), `out/bid-state.json`, `out/bid-context.json` |
|
|
58
58
|
| `bid-pack apply` | `--answers answers.json [--dir bid/]` | records answers; runs pricing + financial proposal on user numbers; `out/cv-evidence.json`, `out/fill-answers.json`, `out/clarifications.md`; regenerates questions |
|
|
59
59
|
| `bid-pack pack` | `--dir bid/ [--fallback-pdf] [--no-zip]` | delegates to `package` |
|
|
60
|
-
| `profile` (`init`) | `init \| set k=v… \| get [k] \| path \| erase` | `~/.tor-to-proposal/profile.json` — user-supplied identity, floor math, rate defaults, master-CV path |
|
|
60
|
+
| `profile` (`init`) | `init \| set k=v… \| get [k] \| path \| erase` | `.tor-to-proposal/profile.json` in the current folder (legacy `~/.tor-to-proposal/profile.json` read as fallback) — user-supplied identity, floor math, rate defaults, master-CV path |
|
|
61
61
|
| `cv-tailor build` | `--cv master.txt --extract tor-extract.json [--profile p.json] [--gap r.md]` | `cv-tailored.md` (reordered master-CV material only) + `cv-tailor-report.md` (trace + terminology mirror) |
|
|
62
62
|
| `cv-tailor lint` | `--cv tailored.md --master master.txt [--gap] [--evidence cv-evidence.json]` | anchor table; exit 1 on unanchored bullets, altered numbers, GAP terms |
|
|
63
63
|
| `financial-proposal` | `--pricing pricing.json --extract tor-extract.json [--context bid-context.json] [--xlsx]` | `financial-proposal.md` (+ `.xlsx` with visible formulas: `D8=B8*C8`, `D10=SUM`) |
|
|
@@ -111,7 +111,7 @@ tor-extract criteria exactly.
|
|
|
111
111
|
|
|
112
112
|
**v2 bid-pack contracts** (all under `<bid-dir>/out/`):
|
|
113
113
|
|
|
114
|
-
-
|
|
114
|
+
- **`.tor-to-proposal/profile.json` (in the current folder)** — `{identity{name,credentials,…},
|
|
115
115
|
rates{floor{annualIncome,billableDays,costLoading},
|
|
116
116
|
defaults{base,basis,currency,loading,contingency}}, cv{masterPath}}`.
|
|
117
117
|
Every value user-supplied (`profile set` validates numbers/ranges and the
|
package/mcp/server.mjs
CHANGED
|
@@ -183,7 +183,7 @@ const TOOLS = [
|
|
|
183
183
|
{
|
|
184
184
|
name: 'profile_set',
|
|
185
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.',
|
|
186
|
+
'One-time consultant profile (stored at .tor-to-proposal/profile.json in the current working folder; legacy ~/.tor-to-proposal/profile.json is read as fallback). 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
187
|
inputSchema: S({
|
|
188
188
|
pairs: { type: 'array', items: { type: 'string' }, description: 'key=value pairs, e.g. "identity.name=Ayesha Khan", "rates.defaults.base=400"' },
|
|
189
189
|
}, ['pairs']),
|
|
@@ -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.
|
|
325
|
+
serverInfo: { name: 'tor-to-proposal', version: '2.0.1' },
|
|
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.
|
|
3
|
+
"version": "2.0.1",
|
|
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": {
|