qiksy 1.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.
- package/ARCHITECTURE.md +78 -0
- package/INSTALL-PROMPT.md +53 -0
- package/bin/qiksy.mjs +302 -0
- package/package.json +32 -0
- package/tools/client-finder/vendor/README.md +70 -0
- package/tools/client-finder/vendor/find.mjs +121 -0
- package/tools/client-finder/vendor/icp/assist-pl-shops.json +30 -0
- package/tools/client-finder/vendor/icp/assist-ua-shops.json +30 -0
- package/tools/client-finder/vendor/knowledge/assist.md +68 -0
- package/tools/client-finder/vendor/knowledge/custom.md +36 -0
- package/tools/client-finder/vendor/knowledge/multilogin.md +29 -0
- package/tools/client-finder/vendor/knowledge/pay.md +26 -0
- package/tools/client-finder/vendor/knowledge/qa-copilot.md +43 -0
- package/tools/client-finder/vendor/knowledge/travel.md +43 -0
- package/tools/client-finder/vendor/knowledge/work-finder.md +52 -0
- package/tools/client-finder/vendor/lib/access.mjs +108 -0
- package/tools/client-finder/vendor/lib/agent.mjs +204 -0
- package/tools/client-finder/vendor/lib/approver-mcp.mjs +78 -0
- package/tools/client-finder/vendor/lib/browser.mjs +108 -0
- package/tools/client-finder/vendor/lib/catalog.mjs +86 -0
- package/tools/client-finder/vendor/lib/channels.mjs +54 -0
- package/tools/client-finder/vendor/lib/deliver.mjs +89 -0
- package/tools/client-finder/vendor/lib/engine.mjs +283 -0
- package/tools/client-finder/vendor/lib/formats.mjs +108 -0
- package/tools/client-finder/vendor/lib/jobs.mjs +75 -0
- package/tools/client-finder/vendor/lib/knowledge.mjs +26 -0
- package/tools/client-finder/vendor/lib/prompts.mjs +183 -0
- package/tools/client-finder/vendor/lib/report.mjs +75 -0
- package/tools/client-finder/vendor/lib/scope.mjs +162 -0
- package/tools/client-finder/vendor/lib/spawn-claude.mjs +44 -0
- package/tools/client-finder/vendor/lib/verify.mjs +96 -0
- package/tools/client-finder/vendor/package.json +8 -0
- package/tools/client-finder/vendor/server.mjs +390 -0
- package/tools/travel/vendor/README.md +58 -0
- package/tools/travel/vendor/docs/CONTRACT.md +174 -0
- package/tools/travel/vendor/docs/ENGINE-MIGRATION.md +33 -0
- package/tools/travel/vendor/lib/agent.mjs +190 -0
- package/tools/travel/vendor/lib/engine.mjs +283 -0
- package/tools/travel/vendor/lib/jobs.mjs +125 -0
- package/tools/travel/vendor/lib/license.mjs +41 -0
- package/tools/travel/vendor/lib/plans.mjs +28 -0
- package/tools/travel/vendor/lib/prompts.mjs +394 -0
- package/tools/travel/vendor/lib/proposal.mjs +207 -0
- package/tools/travel/vendor/lib/spawn-claude.mjs +44 -0
- package/tools/travel/vendor/lib/store.mjs +75 -0
- package/tools/travel/vendor/package.json +12 -0
- package/tools/travel/vendor/public/app.js +996 -0
- package/tools/travel/vendor/public/index.html +112 -0
- package/tools/travel/vendor/public/styles.css +760 -0
- package/tools/travel/vendor/scripts/approver-mcp.mjs +80 -0
- package/tools/travel/vendor/scripts/launch-browser.mjs +77 -0
- package/tools/travel/vendor/server.mjs +755 -0
- package/tools/work-finder/vendor/.claude/settings.local.json +27 -0
- package/tools/work-finder/vendor/.mcp.json +9 -0
- package/tools/work-finder/vendor/CLAUDE.md +24 -0
- package/tools/work-finder/vendor/README.md +44 -0
- package/tools/work-finder/vendor/bin/qiksy-work-finder.mjs +127 -0
- package/tools/work-finder/vendor/lib/agent.mjs +159 -0
- package/tools/work-finder/vendor/lib/catalog.mjs +41 -0
- package/tools/work-finder/vendor/lib/channels.mjs +73 -0
- package/tools/work-finder/vendor/lib/engine.mjs +283 -0
- package/tools/work-finder/vendor/lib/jobs.mjs +127 -0
- package/tools/work-finder/vendor/lib/license.mjs +60 -0
- package/tools/work-finder/vendor/lib/linkcheck.mjs +72 -0
- package/tools/work-finder/vendor/lib/linkedin.mjs +22 -0
- package/tools/work-finder/vendor/lib/plans.mjs +33 -0
- package/tools/work-finder/vendor/lib/prompts.mjs +217 -0
- package/tools/work-finder/vendor/lib/spawn-claude.mjs +44 -0
- package/tools/work-finder/vendor/lib/store.mjs +125 -0
- package/tools/work-finder/vendor/package.json +41 -0
- package/tools/work-finder/vendor/public/app.js +1863 -0
- package/tools/work-finder/vendor/public/index.html +518 -0
- package/tools/work-finder/vendor/public/styles.css +930 -0
- package/tools/work-finder/vendor/scripts/approver-mcp.mjs +69 -0
- package/tools/work-finder/vendor/scripts/extract-linkedin.mjs +81 -0
- package/tools/work-finder/vendor/scripts/launch-browser.mjs +78 -0
- package/tools/work-finder/vendor/scripts/screenshot.mjs +17 -0
- package/tools/work-finder/vendor/server.mjs +1006 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client proposal — the whole selection as ONE page an agency can send.
|
|
3
|
+
*
|
|
4
|
+
* This is the paid feature (owner decision 2026-07-25) and it is aimed at a
|
|
5
|
+
* measured pain: advisors report that a "five-minute" quote turns into two hours of
|
|
6
|
+
* reformatting, because the options live in six browser tabs and the client wants
|
|
7
|
+
* them side by side. Here the agent already has them structured, so the page is a
|
|
8
|
+
* pure rendering job: route, who is going, every option with its price, the
|
|
9
|
+
* recommended combination, and the agency's own name on top.
|
|
10
|
+
*
|
|
11
|
+
* Self-contained by construction — inline CSS, no scripts, no external assets — so
|
|
12
|
+
* it survives being saved to disk, attached to an email, or printed to PDF. It is
|
|
13
|
+
* generated by the LOCAL backend on the agent's own machine; nothing is uploaded.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const CUR = { EUR: '€', USD: '$', UAH: '₴', PLN: 'zł', GBP: '£', CZK: 'Kč', CHF: 'CHF' };
|
|
17
|
+
|
|
18
|
+
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
19
|
+
const safeUrl = u => (/^https?:\/\//i.test(String(u ?? '')) ? String(u) : '');
|
|
20
|
+
const money = (amount, currency) => (amount == null ? '' : `${CUR[currency || ''] || (currency ? currency + ' ' : '')}${amount}`);
|
|
21
|
+
|
|
22
|
+
const T = {
|
|
23
|
+
en: {
|
|
24
|
+
proposal: 'Trip proposal', who: 'Travellers', dates: 'Dates', route: 'Route',
|
|
25
|
+
transport: 'Transport', hotel: 'Stay', transfer: 'Transfer', other: 'Also',
|
|
26
|
+
recommended: 'Our recommendation', total: 'total', perNight: 'per night',
|
|
27
|
+
open: 'View', nights: n => `${n} night${n === 1 ? '' : 's'}`, oneWay: 'one way',
|
|
28
|
+
prepared: 'Prepared', direct: 'Direct', pick: 'Pick the option you like and reply — we book it.',
|
|
29
|
+
budget: 'Budget', notes: 'Notes',
|
|
30
|
+
},
|
|
31
|
+
ru: {
|
|
32
|
+
proposal: 'Предложение по поездке', who: 'Кто едет', dates: 'Даты', route: 'Маршрут',
|
|
33
|
+
transport: 'Транспорт', hotel: 'Проживание', transfer: 'Трансфер', other: 'Ещё',
|
|
34
|
+
recommended: 'Наша рекомендация', total: 'итого', perNight: 'за ночь',
|
|
35
|
+
open: 'Смотреть', nights: n => `${n} ${n % 10 === 1 && n % 100 !== 11 ? 'ночь' : [2, 3, 4].includes(n % 10) && ![12, 13, 14].includes(n % 100) ? 'ночи' : 'ночей'}`,
|
|
36
|
+
oneWay: 'в одну сторону', prepared: 'Подготовлено', direct: 'Без пересадок',
|
|
37
|
+
pick: 'Выберите вариант и ответьте — мы забронируем.', budget: 'Бюджет', notes: 'Примечания',
|
|
38
|
+
},
|
|
39
|
+
uk: {
|
|
40
|
+
proposal: 'Пропозиція щодо поїздки', who: 'Хто їде', dates: 'Дати', route: 'Маршрут',
|
|
41
|
+
transport: 'Транспорт', hotel: 'Проживання', transfer: 'Трансфер', other: 'Ще',
|
|
42
|
+
recommended: 'Наша рекомендація', total: 'разом', perNight: 'за ніч',
|
|
43
|
+
open: 'Дивитись', nights: n => `${n} ${n % 10 === 1 && n % 100 !== 11 ? 'ніч' : [2, 3, 4].includes(n % 10) && ![12, 13, 14].includes(n % 100) ? 'ночі' : 'ночей'}`,
|
|
44
|
+
oneWay: 'в один бік', prepared: 'Підготовлено', direct: 'Без пересадок',
|
|
45
|
+
pick: 'Оберіть варіант і дайте відповідь — ми забронюємо.', budget: 'Бюджет', notes: 'Примітки',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const MONTHS = {
|
|
50
|
+
en: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
|
51
|
+
ru: ['янв', 'фев', 'мар', 'апр', 'мая', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'],
|
|
52
|
+
uk: ['січ', 'лют', 'бер', 'кві', 'тра', 'чер', 'лип', 'сер', 'вер', 'жов', 'лис', 'гру'],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
function fmtDate(iso, lang) {
|
|
56
|
+
if (!iso) return '';
|
|
57
|
+
const [, m, d] = String(iso).split('-');
|
|
58
|
+
if (!m || !d) return iso;
|
|
59
|
+
return `${Number(d)} ${MONTHS[lang][Number(m) - 1] || ''}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function nightsOf(trip) {
|
|
63
|
+
if (!trip.departDate || !trip.returnDate) return 0;
|
|
64
|
+
const n = Math.round((Date.parse(trip.returnDate) - Date.parse(trip.departDate)) / 86_400_000);
|
|
65
|
+
return n > 0 ? n : 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function optionCard(o, t, lang) {
|
|
69
|
+
const p = o.price || {};
|
|
70
|
+
const price = money(p.amount, p.currency);
|
|
71
|
+
const unit = p.unit === 'per_night' ? t.perNight : t.total;
|
|
72
|
+
const link = safeUrl(o.link);
|
|
73
|
+
const meta = [];
|
|
74
|
+
if (o.kind === 'hotel') {
|
|
75
|
+
if (o.stars) meta.push('★'.repeat(Math.max(1, Math.min(5, o.stars))));
|
|
76
|
+
if (o.rating?.score != null) meta.push(`${o.rating.score}${o.rating.reviews != null ? ` · ${o.rating.reviews}` : ''}`);
|
|
77
|
+
if (o.address) meta.push(esc(o.address));
|
|
78
|
+
if (o.distanceKm != null) meta.push(`${o.distanceKm} km`);
|
|
79
|
+
} else if (['flight', 'train', 'bus'].includes(o.kind)) {
|
|
80
|
+
const dep = o.depart || {}, arr = o.arrive || {};
|
|
81
|
+
if (dep.time || arr.time) meta.push(`${esc(dep.time || '')} → ${esc(arr.time || '')}`);
|
|
82
|
+
if (dep.place || arr.place) meta.push(`${esc(dep.place || '')} → ${esc(arr.place || '')}`);
|
|
83
|
+
if (o.duration) meta.push(esc(o.duration));
|
|
84
|
+
if (o.direct) meta.push(t.direct);
|
|
85
|
+
if (o.baggage) meta.push(esc(o.baggage));
|
|
86
|
+
} else {
|
|
87
|
+
if (o.service) meta.push(esc(o.service));
|
|
88
|
+
if (o.vehicle) meta.push(esc(o.vehicle));
|
|
89
|
+
if (o.pickupAt) meta.push(esc(o.pickupAt));
|
|
90
|
+
}
|
|
91
|
+
const extra = o.kind === 'hotel' && o.totalPrice != null
|
|
92
|
+
? `<div class="extra">${t.total} ${esc(money(o.totalPrice, p.currency))}${o.nights ? ` · ${t.nights(o.nights)}` : ''}</div>`
|
|
93
|
+
: '';
|
|
94
|
+
return `
|
|
95
|
+
<article class="opt">
|
|
96
|
+
<div class="opt-n">${esc(o.n)}</div>
|
|
97
|
+
<div class="opt-main">
|
|
98
|
+
<h3>${esc(o.title)}${o.brand && o.brand !== o.title ? ` <span class="brand">· ${esc(o.brand)}</span>` : ''}</h3>
|
|
99
|
+
${meta.length ? `<div class="meta">${meta.join(' · ')}</div>` : ''}
|
|
100
|
+
${o.notes ? `<p class="notes">${esc(o.notes)}</p>` : ''}
|
|
101
|
+
${link ? `<a class="link" href="${esc(link)}" target="_blank" rel="noopener">${t.open} →</a>` : ''}
|
|
102
|
+
</div>
|
|
103
|
+
<div class="opt-price">
|
|
104
|
+
${price ? `<div class="unit">${esc(unit)}</div><div class="amount">${esc(price)}</div>` : ''}
|
|
105
|
+
${extra}
|
|
106
|
+
</div>
|
|
107
|
+
</article>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {object} a
|
|
112
|
+
* @param {object} a.trip
|
|
113
|
+
* @param {object|null} a.party the client this trip belongs to
|
|
114
|
+
* @param {object[]} a.travellers
|
|
115
|
+
* @param {object} a.brand { name, contact, note } — the agency's own header
|
|
116
|
+
* @param {'en'|'ru'|'uk'} a.lang
|
|
117
|
+
*/
|
|
118
|
+
export function buildProposalHtml({ trip, party, travellers = [], brand = {}, lang = 'en' }) {
|
|
119
|
+
const t = T[lang] || T.en;
|
|
120
|
+
const opts = [...(trip.options || [])].sort((a, b) => (a.n || 0) - (b.n || 0));
|
|
121
|
+
const known = new Set(['flight', 'train', 'bus', 'hotel', 'transfer']);
|
|
122
|
+
const groups = [
|
|
123
|
+
{ label: t.transport, items: opts.filter(o => ['flight', 'train', 'bus'].includes(o.kind)) },
|
|
124
|
+
{ label: t.hotel, items: opts.filter(o => o.kind === 'hotel') },
|
|
125
|
+
{ label: t.transfer, items: opts.filter(o => o.kind === 'transfer') },
|
|
126
|
+
{ label: t.other, items: opts.filter(o => !known.has(o.kind)) },
|
|
127
|
+
].filter(g => g.items.length);
|
|
128
|
+
|
|
129
|
+
const n = nightsOf(trip);
|
|
130
|
+
const dates = trip.returnDate
|
|
131
|
+
? `${fmtDate(trip.departDate, lang)} → ${fmtDate(trip.returnDate, lang)}${n ? ` · ${t.nights(n)}` : ''}`
|
|
132
|
+
: `${fmtDate(trip.departDate, lang)} · ${t.oneWay}`;
|
|
133
|
+
const who = travellers.map(m => m.nameRu || m.nameEn).filter(Boolean).join(', ');
|
|
134
|
+
const rec = trip.recommendation || {};
|
|
135
|
+
const title = `${trip.from} → ${trip.to}`;
|
|
136
|
+
|
|
137
|
+
return `<!doctype html>
|
|
138
|
+
<html lang="${lang}">
|
|
139
|
+
<meta charset="utf-8">
|
|
140
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
141
|
+
<title>${esc(brand.name ? brand.name + ' — ' : '')}${esc(title)}</title>
|
|
142
|
+
<style>
|
|
143
|
+
:root { --ink:#112a2d; --ink2:#4a6165; --ink3:#8aa2a4; --line:#dde8e8; --teal:#17948f; --bg:#f4f8f8; --card:#fff; }
|
|
144
|
+
* { box-sizing: border-box; }
|
|
145
|
+
body { margin:0; background:var(--bg); color:var(--ink);
|
|
146
|
+
font:15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif; }
|
|
147
|
+
.wrap { max-width: 880px; margin: 0 auto; padding: 32px 20px 64px; }
|
|
148
|
+
header.brand { display:flex; align-items:baseline; gap:12px; flex-wrap:wrap; margin-bottom: 26px; }
|
|
149
|
+
.brand-name { font-size:16px; font-weight:800; letter-spacing:-.2px; }
|
|
150
|
+
.brand-contact { color:var(--ink3); font-size:13px; }
|
|
151
|
+
.hero { background:linear-gradient(120deg,#0b3d40,#0f766e); color:#fff; border-radius:20px; padding:26px 28px; }
|
|
152
|
+
.hero .eyebrow { text-transform:uppercase; letter-spacing:1.6px; font-size:11px; opacity:.85; }
|
|
153
|
+
.hero h1 { margin:8px 0 12px; font-size:30px; letter-spacing:-.6px; }
|
|
154
|
+
.hero .facts { display:flex; gap:22px; flex-wrap:wrap; font-size:13.5px; opacity:.95; }
|
|
155
|
+
.lead { margin:22px 0 6px; color:var(--ink2); }
|
|
156
|
+
.rec { background:#e9f8ef; border:1px solid #b8e6c9; border-radius:16px; padding:16px 18px; margin:20px 0 8px; }
|
|
157
|
+
.rec h2 { margin:0 0 6px; font-size:14px; text-transform:uppercase; letter-spacing:1.2px; color:#15803d; }
|
|
158
|
+
h2.sec { font-size:12px; text-transform:uppercase; letter-spacing:1.4px; color:var(--teal); margin:28px 0 10px; }
|
|
159
|
+
.opt { display:flex; gap:16px; background:var(--card); border:1px solid var(--line); border-radius:16px;
|
|
160
|
+
padding:16px 18px; margin-bottom:12px; page-break-inside:avoid; }
|
|
161
|
+
.opt-n { flex:none; width:28px; height:28px; border-radius:9px; background:#eef5f5; color:var(--teal);
|
|
162
|
+
font-weight:800; display:flex; align-items:center; justify-content:center; font-size:13px; }
|
|
163
|
+
.opt-main { flex:1; min-width:0; }
|
|
164
|
+
.opt-main h3 { margin:0 0 4px; font-size:16px; letter-spacing:-.2px; }
|
|
165
|
+
.brand { color:var(--ink3); font-weight:500; }
|
|
166
|
+
.meta { color:var(--ink2); font-size:13px; }
|
|
167
|
+
.notes { color:var(--ink2); font-size:13px; margin:6px 0 0; }
|
|
168
|
+
.link { display:inline-block; margin-top:8px; color:var(--teal); font-size:13px; font-weight:600; text-decoration:none; }
|
|
169
|
+
.opt-price { flex:none; text-align:right; min-width:110px; }
|
|
170
|
+
.unit { font-size:11px; text-transform:uppercase; letter-spacing:.8px; color:var(--ink3); }
|
|
171
|
+
.amount { font-size:20px; font-weight:800; letter-spacing:-.4px; }
|
|
172
|
+
.extra { font-size:12px; color:var(--ink2); margin-top:2px; }
|
|
173
|
+
footer { margin-top:34px; padding-top:16px; border-top:1px solid var(--line); color:var(--ink3); font-size:12.5px; }
|
|
174
|
+
@media print { body { background:#fff; } .wrap { padding:0; } .opt { box-shadow:none; } }
|
|
175
|
+
</style>
|
|
176
|
+
<div class="wrap">
|
|
177
|
+
${brand.name || brand.contact ? `<header class="brand">
|
|
178
|
+
${brand.name ? `<span class="brand-name">${esc(brand.name)}</span>` : ''}
|
|
179
|
+
${brand.contact ? `<span class="brand-contact">${esc(brand.contact)}</span>` : ''}
|
|
180
|
+
</header>` : ''}
|
|
181
|
+
|
|
182
|
+
<section class="hero">
|
|
183
|
+
<div class="eyebrow">${esc(party && party.kind === 'client' && party.name ? party.name : t.proposal)}</div>
|
|
184
|
+
<h1>${esc(trip.from)} → ${esc(trip.to)}</h1>
|
|
185
|
+
<div class="facts">
|
|
186
|
+
<span>${esc(dates)}</span>
|
|
187
|
+
${who ? `<span>${esc(t.who)}: ${esc(who)}</span>` : ''}
|
|
188
|
+
${trip.budget ? `<span>${esc(t.budget)}: ${esc(trip.budget)}</span>` : ''}
|
|
189
|
+
</div>
|
|
190
|
+
</section>
|
|
191
|
+
|
|
192
|
+
${trip.summary ? `<p class="lead">${esc(trip.summary)}</p>` : ''}
|
|
193
|
+
|
|
194
|
+
${rec.text || (rec.ids || []).length ? `<div class="rec">
|
|
195
|
+
<h2>${esc(t.recommended)}</h2>
|
|
196
|
+
${rec.text ? `<div>${esc(rec.text)}</div>` : ''}
|
|
197
|
+
${(rec.ids || []).length ? `<div class="meta">№ ${(rec.ids || []).map(x => esc(x)).join(' + ')}${rec.comboTotal ? ` · ${esc(rec.comboTotal)}` : ''}</div>` : ''}
|
|
198
|
+
</div>` : ''}
|
|
199
|
+
|
|
200
|
+
${groups.map(g => `<h2 class="sec">${esc(g.label)}</h2>${g.items.map(o => optionCard(o, t, lang)).join('')}`).join('')}
|
|
201
|
+
|
|
202
|
+
${brand.note ? `<p class="lead">${esc(brand.note)}</p>` : `<p class="lead">${esc(t.pick)}</p>`}
|
|
203
|
+
|
|
204
|
+
<footer>${esc(t.prepared)}: ${new Date().toLocaleDateString(lang === 'en' ? 'en-GB' : lang === 'uk' ? 'uk-UA' : 'ru-RU')}${brand.name ? ` · ${esc(brand.name)}` : ''}</footer>
|
|
205
|
+
</div>
|
|
206
|
+
</html>`;
|
|
207
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Start the `claude` CLI — on any operating system.
|
|
3
|
+
*
|
|
4
|
+
* On macOS and Linux `claude` on PATH is a real executable and Node's own spawn finds
|
|
5
|
+
* it. ON WINDOWS IT IS NOT: npm installs a `claude.cmd` shim, and Node cannot execute
|
|
6
|
+
* a .cmd without a shell — recent versions refuse outright (the fix for CVE-2024-27980).
|
|
7
|
+
* So every call landed on ENOENT and the panel announced "Claude Code is not installed"
|
|
8
|
+
* on a machine where it was installed and working perfectly. The probe, the sign-in and
|
|
9
|
+
* the agent runs all went through this one door, so on Windows nothing worked at all.
|
|
10
|
+
*
|
|
11
|
+
* `shell: true` would make the error go away and open a hole instead: the agent passes
|
|
12
|
+
* the user's OWN TEXT in argv — a resume, a job description, an MCP config as JSON —
|
|
13
|
+
* and through cmd.exe every `&`, `|`, `^` and quote in it becomes a shell metacharacter.
|
|
14
|
+
* cross-spawn is the standard answer to exactly this (npm itself runs on it): it
|
|
15
|
+
* resolves the shim and escapes arguments for cmd.exe properly.
|
|
16
|
+
*
|
|
17
|
+
* Imported LAZILY, with a fallback, so the backends keep the "no dependencies"
|
|
18
|
+
* property they were written for: run from this monorepo they get cross-spawn, run
|
|
19
|
+
* standalone on macOS or Linux they fall back to Node's spawn — which is exactly what
|
|
20
|
+
* they used before this file existed.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
24
|
+
|
|
25
|
+
let impl = null;
|
|
26
|
+
try {
|
|
27
|
+
impl = (await import('cross-spawn')).default;
|
|
28
|
+
} catch {
|
|
29
|
+
// Only Windows actually needs it; saying so anywhere else would be noise.
|
|
30
|
+
if (process.platform === 'win32') {
|
|
31
|
+
console.warn(
|
|
32
|
+
'[claude] cross-spawn is missing, and on Windows the CLI cannot be started without it.\n' +
|
|
33
|
+
' Run `npm install` in the qiksy-studio root, then start this server again.',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string[]} args argv for the CLI
|
|
40
|
+
* @param {import('node:child_process').SpawnOptions} [opts]
|
|
41
|
+
*/
|
|
42
|
+
export function spawnClaude(args, opts = {}) {
|
|
43
|
+
return (impl ?? nodeSpawn)('claude', args, opts);
|
|
44
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** JSON file store: data/db.json. Zero deps, sync IO (single-user tool). */
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from 'node:fs';
|
|
3
|
+
import { resolve, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
export const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
7
|
+
const DB_PATH = process.env.TA_DB ? resolve(process.env.TA_DB) : resolve(ROOT, 'data/db.json');
|
|
8
|
+
|
|
9
|
+
export const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
|
10
|
+
|
|
11
|
+
const CORPORATE_DEFAULTS = {
|
|
12
|
+
legalEntity: '', travelAgency: '', geniusLevel: '', airlineCodes: '',
|
|
13
|
+
insurance: '', transferVendor: '',
|
|
14
|
+
flightClass: 'эконом', trainClass: '2 класс', hotelStars: '3-4*',
|
|
15
|
+
perDiem: '', hrTimezone: 'Europe/Kyiv', currency: 'EUR',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// plan: 'free' = ОДНА группа (сам + семья); вести чужие поездки (несколько
|
|
19
|
+
// клиентов) — платно. 'dev' (наш инстанс) снимает лимит — руками в db.json.
|
|
20
|
+
// plan: ручной выбор ограничен free/dev; платный pro включается лицензией.
|
|
21
|
+
// license — ключ (tr_…), licenseInfo — кэш последней проверки у воркера.
|
|
22
|
+
// brand* — шапка клиентского предложения (Pro): агентство ставит на страницу своё
|
|
23
|
+
// имя и контакт, потому что отправляет её клиенту от своего лица, не от нашего.
|
|
24
|
+
const SETTINGS_DEFAULTS = {
|
|
25
|
+
model: 'sonnet', browserSearch: false, extraInstructions: '',
|
|
26
|
+
plan: 'free', license: '', licenseInfo: null,
|
|
27
|
+
brandName: '', brandContact: '', brandNote: '',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Своя группа — «я и моя семья». Есть всегда, создаётся молча: на free человек
|
|
31
|
+
* вообще не должен видеть слово «клиент», он просто добавляет попутчиков. */
|
|
32
|
+
export const SELF_PARTY_ID = 'p_self';
|
|
33
|
+
const selfParty = () => ({ id: SELF_PARTY_ID, kind: 'self', name: '', email: '', phone: '', note: '', createdAt: Date.now() });
|
|
34
|
+
|
|
35
|
+
// A fresh install starts EMPTY — no demo travellers, no demo trips (owner decision
|
|
36
|
+
// 2026-07-19: every flow begins from zero; the UI's empty states guide the first step).
|
|
37
|
+
function defaults() {
|
|
38
|
+
return {
|
|
39
|
+
// parties = «кто едет»: на free одна (своя семья), на Pro — по группе на клиента.
|
|
40
|
+
parties: [selfParty()],
|
|
41
|
+
team: [],
|
|
42
|
+
corporate: { ...CORPORATE_DEFAULTS },
|
|
43
|
+
trips: [],
|
|
44
|
+
settings: { ...SETTINGS_DEFAULTS },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function load() {
|
|
49
|
+
if (!existsSync(DB_PATH)) {
|
|
50
|
+
const db = defaults();
|
|
51
|
+
save(db);
|
|
52
|
+
return db;
|
|
53
|
+
}
|
|
54
|
+
const db = JSON.parse(readFileSync(DB_PATH, 'utf8'));
|
|
55
|
+
// migrate: fill keys added after the db was first created
|
|
56
|
+
db.team ||= [];
|
|
57
|
+
db.trips ||= [];
|
|
58
|
+
db.corporate = { ...CORPORATE_DEFAULTS, ...db.corporate };
|
|
59
|
+
db.settings = { ...SETTINGS_DEFAULTS, ...db.settings };
|
|
60
|
+
// Groups arrived after the flat team list: everything that existed before is
|
|
61
|
+
// the owner's own party, so nothing needs re-assigning by hand on upgrade.
|
|
62
|
+
if (!Array.isArray(db.parties) || !db.parties.length) db.parties = [selfParty()];
|
|
63
|
+
for (const m of db.team) m.partyId ||= db.parties[0].id;
|
|
64
|
+
for (const t of db.trips) t.partyId ||= db.parties[0].id;
|
|
65
|
+
return db;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function save(db) {
|
|
69
|
+
mkdirSync(dirname(DB_PATH), { recursive: true });
|
|
70
|
+
// Write to a temp file then rename over db.json — atomic on the same FS, so a
|
|
71
|
+
// crash mid-write leaves the previous db.json intact instead of a truncated one.
|
|
72
|
+
const tmp = DB_PATH + '.tmp';
|
|
73
|
+
writeFileSync(tmp, JSON.stringify(db, null, 2), 'utf8');
|
|
74
|
+
renameSync(tmp, DB_PATH);
|
|
75
|
+
}
|