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.
Files changed (78) hide show
  1. package/ARCHITECTURE.md +78 -0
  2. package/INSTALL-PROMPT.md +53 -0
  3. package/bin/qiksy.mjs +302 -0
  4. package/package.json +32 -0
  5. package/tools/client-finder/vendor/README.md +70 -0
  6. package/tools/client-finder/vendor/find.mjs +121 -0
  7. package/tools/client-finder/vendor/icp/assist-pl-shops.json +30 -0
  8. package/tools/client-finder/vendor/icp/assist-ua-shops.json +30 -0
  9. package/tools/client-finder/vendor/knowledge/assist.md +68 -0
  10. package/tools/client-finder/vendor/knowledge/custom.md +36 -0
  11. package/tools/client-finder/vendor/knowledge/multilogin.md +29 -0
  12. package/tools/client-finder/vendor/knowledge/pay.md +26 -0
  13. package/tools/client-finder/vendor/knowledge/qa-copilot.md +43 -0
  14. package/tools/client-finder/vendor/knowledge/travel.md +43 -0
  15. package/tools/client-finder/vendor/knowledge/work-finder.md +52 -0
  16. package/tools/client-finder/vendor/lib/access.mjs +108 -0
  17. package/tools/client-finder/vendor/lib/agent.mjs +204 -0
  18. package/tools/client-finder/vendor/lib/approver-mcp.mjs +78 -0
  19. package/tools/client-finder/vendor/lib/browser.mjs +108 -0
  20. package/tools/client-finder/vendor/lib/catalog.mjs +86 -0
  21. package/tools/client-finder/vendor/lib/channels.mjs +54 -0
  22. package/tools/client-finder/vendor/lib/deliver.mjs +89 -0
  23. package/tools/client-finder/vendor/lib/engine.mjs +283 -0
  24. package/tools/client-finder/vendor/lib/formats.mjs +108 -0
  25. package/tools/client-finder/vendor/lib/jobs.mjs +75 -0
  26. package/tools/client-finder/vendor/lib/knowledge.mjs +26 -0
  27. package/tools/client-finder/vendor/lib/prompts.mjs +183 -0
  28. package/tools/client-finder/vendor/lib/report.mjs +75 -0
  29. package/tools/client-finder/vendor/lib/scope.mjs +162 -0
  30. package/tools/client-finder/vendor/lib/spawn-claude.mjs +44 -0
  31. package/tools/client-finder/vendor/lib/verify.mjs +96 -0
  32. package/tools/client-finder/vendor/package.json +8 -0
  33. package/tools/client-finder/vendor/server.mjs +390 -0
  34. package/tools/travel/vendor/README.md +58 -0
  35. package/tools/travel/vendor/docs/CONTRACT.md +174 -0
  36. package/tools/travel/vendor/docs/ENGINE-MIGRATION.md +33 -0
  37. package/tools/travel/vendor/lib/agent.mjs +190 -0
  38. package/tools/travel/vendor/lib/engine.mjs +283 -0
  39. package/tools/travel/vendor/lib/jobs.mjs +125 -0
  40. package/tools/travel/vendor/lib/license.mjs +41 -0
  41. package/tools/travel/vendor/lib/plans.mjs +28 -0
  42. package/tools/travel/vendor/lib/prompts.mjs +394 -0
  43. package/tools/travel/vendor/lib/proposal.mjs +207 -0
  44. package/tools/travel/vendor/lib/spawn-claude.mjs +44 -0
  45. package/tools/travel/vendor/lib/store.mjs +75 -0
  46. package/tools/travel/vendor/package.json +12 -0
  47. package/tools/travel/vendor/public/app.js +996 -0
  48. package/tools/travel/vendor/public/index.html +112 -0
  49. package/tools/travel/vendor/public/styles.css +760 -0
  50. package/tools/travel/vendor/scripts/approver-mcp.mjs +80 -0
  51. package/tools/travel/vendor/scripts/launch-browser.mjs +77 -0
  52. package/tools/travel/vendor/server.mjs +755 -0
  53. package/tools/work-finder/vendor/.claude/settings.local.json +27 -0
  54. package/tools/work-finder/vendor/.mcp.json +9 -0
  55. package/tools/work-finder/vendor/CLAUDE.md +24 -0
  56. package/tools/work-finder/vendor/README.md +44 -0
  57. package/tools/work-finder/vendor/bin/qiksy-work-finder.mjs +127 -0
  58. package/tools/work-finder/vendor/lib/agent.mjs +159 -0
  59. package/tools/work-finder/vendor/lib/catalog.mjs +41 -0
  60. package/tools/work-finder/vendor/lib/channels.mjs +73 -0
  61. package/tools/work-finder/vendor/lib/engine.mjs +283 -0
  62. package/tools/work-finder/vendor/lib/jobs.mjs +127 -0
  63. package/tools/work-finder/vendor/lib/license.mjs +60 -0
  64. package/tools/work-finder/vendor/lib/linkcheck.mjs +72 -0
  65. package/tools/work-finder/vendor/lib/linkedin.mjs +22 -0
  66. package/tools/work-finder/vendor/lib/plans.mjs +33 -0
  67. package/tools/work-finder/vendor/lib/prompts.mjs +217 -0
  68. package/tools/work-finder/vendor/lib/spawn-claude.mjs +44 -0
  69. package/tools/work-finder/vendor/lib/store.mjs +125 -0
  70. package/tools/work-finder/vendor/package.json +41 -0
  71. package/tools/work-finder/vendor/public/app.js +1863 -0
  72. package/tools/work-finder/vendor/public/index.html +518 -0
  73. package/tools/work-finder/vendor/public/styles.css +930 -0
  74. package/tools/work-finder/vendor/scripts/approver-mcp.mjs +69 -0
  75. package/tools/work-finder/vendor/scripts/extract-linkedin.mjs +81 -0
  76. package/tools/work-finder/vendor/scripts/launch-browser.mjs +78 -0
  77. package/tools/work-finder/vendor/scripts/screenshot.mjs +17 -0
  78. package/tools/work-finder/vendor/server.mjs +1006 -0
@@ -0,0 +1,78 @@
1
+ # Qiksy Engine — one local engine for every local-first tool
2
+
3
+ **Owner decision 2026-07-25:** one engine, not three. Work Finder, Travel and Client
4
+ Finder are ~75% the same backend (`spawn-claude.mjs` is byte-identical across them;
5
+ `agent/jobs/engine/store/license/plans` differ by only 15–43 lines). So they collapse into
6
+ ONE process, ONE port, ONE installer — and a new tool is a new MODULE, not a new installer.
7
+
8
+ The owner chose the **clean** shape: a shared CORE + tool MODULES mounted under namespaced
9
+ routes on a single port.
10
+
11
+ ## Shape
12
+
13
+ ```
14
+ qiksy-engine/
15
+ bin/qiksy.mjs # branded launcher — starts the engine, checks Claude, opens the UI
16
+ server.mjs # ONE http server, ONE port (5544). Shared CORS + /api/engine.
17
+ # Dispatches /api/<tool>/… → that tool module's handler.
18
+ core/
19
+ spawn-claude.mjs # start `claude` cross-platform (byte-identical today)
20
+ agent.mjs # headless `claude -p` runner + approver-MCP + tool/mcp restrictions
21
+ jobs.mjs # in-memory job registry + concurrency queue
22
+ engine.mjs # Claude sign-in / status (ONE for all tools → /api/engine/*)
23
+ store.mjs # JSON store base; DATA_DIR is PER-TOOL (…/wf, …/travel, …/cf)
24
+ browser.mjs # shared CDP Chrome on :9222 (auto-apply / imports)
25
+ http.mjs # tiny helpers: json(), body(), cors(), notFound()
26
+ tools/
27
+ work-finder/ { id:'wf', mount(ctx) → handler, seed() }
28
+ travel/ { id:'travel', … }
29
+ client-finder/{ id:'cf', … }
30
+ ```
31
+
32
+ ## Module contract
33
+
34
+ Each tool exports:
35
+
36
+ ```js
37
+ export default {
38
+ id: 'wf', // route namespace: /api/wf/…
39
+ dataSub: 'wf', // DATA_DIR/<dataSub> for this tool's db + files
40
+ defaults, // fresh-install db shape
41
+ // Given the shared core (jobs, agent runner, browser, per-tool store), return an async
42
+ // (req, res, seg) → handled:boolean. `seg` has the /api/<id> prefix already stripped,
43
+ // so a module's routing is IDENTICAL to its standalone backend today.
44
+ mount(ctx) { return async (req, res, seg) => { … } },
45
+ };
46
+ ```
47
+
48
+ The engine owns: the http server, CORS (the shared UI-origin allow-list + Chrome
49
+ Private-Network preflight), `/api/engine/*` (sign-in is one Claude for all tools), and
50
+ static UI serving. Everything tool-specific stays in the module — routes, prompts, schema.
51
+
52
+ ## Migration (staged; WF keeps working throughout)
53
+
54
+ 1. **Core + server skeleton** (this stage): the package, the branded launcher, the
55
+ module-mounting server with shared CORS + `/api/engine`. New port while unproven.
56
+ 2. **WF module**: refactor `work-finder-backend`'s request handler into a `mount()` — its
57
+ big segment-switch moves in almost verbatim (the `/api` prefix strip makes `seg` match).
58
+ Verify WF works through the engine on the test port. Then point the WF frontend at
59
+ `http://localhost:5544/api/wf` and retire the old standalone backend.
60
+ 3. **Client Finder module**: same, from `client-finder-backend`.
61
+ 4. **Travel module**: LAST and coordinated — a second session is actively editing
62
+ `products/travel-assistant` + shared components right now; fold Travel in once that work
63
+ settles, so we don't clobber each other.
64
+ 5. **Installer**: the launcher (`bin/qiksy.mjs`) becomes the one thing every installer
65
+ (npx → desktop app) starts. `bin/qiksy-work-finder.mjs` (the WF-only launcher already
66
+ committed) is the stepping stone; it generalises to `qiksy` here.
67
+
68
+ ## Frontends
69
+
70
+ Each app changes only its API base: `http://localhost:5544` → `http://localhost:5544/api/<id>`
71
+ (and the engine sign-in stays at `/api/engine`). One line per app (`API_BASE` in
72
+ `wfController.ts` / Travel `controller.ts` / CF). Nothing else in the UIs moves.
73
+
74
+ ## Data
75
+
76
+ `WF_DATA_DIR` (the launcher sets `~/.qiksy`) holds a subfolder per tool — `…/wf`,
77
+ `…/travel`, `…/cf` — so the three JSON stores never collide. The CDP browser profile is
78
+ shared (one Chrome).
@@ -0,0 +1,53 @@
1
+ # The "paste this to Claude" install prompt
2
+
3
+ The simplest install path for a non-technical user (HR, agencies) who already has **Claude
4
+ Code** — the one real prerequisite (it is also the engine's fuel). They copy the block
5
+ below, paste it into Claude Code, and Claude does the whole setup on their machine. The
6
+ Work Finder page meanwhile polls `localhost:5544` and flips to the tool the moment the
7
+ engine answers.
8
+
9
+ This is the canonical text. The app surfaces it with a Copy button; keep the two in sync.
10
+
11
+ ---
12
+
13
+ ```
14
+ You are setting up "Qiksy Work Finder" on this computer for a non-technical person. Do it
15
+ for them, one step at a time, and keep every message short and plain — no jargon.
16
+
17
+ You can run shell commands. Go:
18
+
19
+ 1. Check Node.js: run `node -v`. If it's missing or older than v18, install it the simplest
20
+ way for this OS and tell them what you're doing in one line:
21
+ - macOS: `brew install node` (if no Homebrew, install it first from https://brew.sh)
22
+ - Windows: `winget install OpenJS.NodeJS.LTS`
23
+ - Linux: use the distro package manager (e.g. `sudo apt-get install -y nodejs npm`)
24
+ If a step needs their password or a click, tell them exactly what to do and wait.
25
+
26
+ 2. Start the engine (this downloads it the first time, then keeps it running):
27
+ `npx -y qiksy-work-finder`
28
+ Run it so it KEEPS RUNNING in the background — do not block on it. Then wait ~10 seconds.
29
+
30
+ 3. Confirm it's up: fetch `http://localhost:5544/api/engine`. Expect JSON with
31
+ `"running": true`. If `"claude": false` in that JSON, tell them Claude Code needs to be
32
+ signed in — the Work Finder page will show a "Connect your Claude" button for that; they
33
+ don't need to do anything in the terminal.
34
+
35
+ 4. When it's running, say exactly this and stop:
36
+ "✅ Work Finder is running. Go back to the Work Finder tab in your browser — it will
37
+ connect automatically. Keep THIS window open while you use it."
38
+
39
+ Rules: never delete or change files that aren't part of this setup; if anything fails,
40
+ explain in one plain sentence what happened and what they can try, don't dump errors.
41
+ ```
42
+
43
+ ---
44
+
45
+ ## Notes
46
+
47
+ - **Persistence:** while this Claude window stays open, the engine runs. For always-on
48
+ (survives a reboot) the desktop app / a login agent is the later, nicer path.
49
+ - **Publishing:** the `npx -y qiksy-work-finder` line needs the engine published to npm
50
+ first (owner `npm login`). Until then this prompt is staged, not live.
51
+ - **Two doors, same page:** "paste to Claude" (this) is the fast door for anyone who has
52
+ Claude Code; the signed desktop app (double-click, its wizard even installs Claude Code)
53
+ is the later door for people who want zero terminal at all.
package/bin/qiksy.mjs ADDED
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The Qiksy engine launcher — ONE command that brings up every Qiksy local-first tool.
4
+ *
5
+ * It supervises the three backends as separate processes on their own ports rather than
6
+ * importing them into one server. That is deliberate:
7
+ * · the web apps already talk to 5544 / 5545 / 5546, so nothing on the front end has
8
+ * to change and a half-finished engine can never take the tools down;
9
+ * · each backend keeps its own lifecycle — one crashing does not kill the others;
10
+ * · the backends are edited daily (sometimes by a parallel session), and this way the
11
+ * launcher never has to reach inside their request handling.
12
+ *
13
+ * Everything runs on the user's machine, on their own Claude. Nothing is uploaded.
14
+ */
15
+ import { spawn, spawnSync } from 'node:child_process';
16
+ import { existsSync, mkdirSync } from 'node:fs';
17
+ import { homedir } from 'node:os';
18
+ import { dirname, join, resolve } from 'node:path';
19
+ import { platform } from 'node:process';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const HERE = dirname(fileURLToPath(import.meta.url));
23
+ const PKG = resolve(HERE, '..');
24
+ const isWin = platform === 'win32';
25
+
26
+ const c = {
27
+ b: (s) => `\x1b[1m${s}\x1b[0m`,
28
+ dim: (s) => `\x1b[2m${s}\x1b[0m`,
29
+ blue: (s) => `\x1b[38;5;69m${s}\x1b[0m`,
30
+ green: (s) => `\x1b[32m${s}\x1b[0m`,
31
+ amber: (s) => `\x1b[33m${s}\x1b[0m`,
32
+ red: (s) => `\x1b[31m${s}\x1b[0m`,
33
+ };
34
+
35
+ /** One data home, one subfolder per tool — never inside the npx cache. */
36
+ const DATA_HOME = process.env.QIKSY_HOME || join(homedir(), '.qiksy');
37
+ try {
38
+ mkdirSync(DATA_HOME, { recursive: true });
39
+ } catch {
40
+ /* unwritable home: each backend falls back to its own folder */
41
+ }
42
+
43
+ const TOOLS = [
44
+ {
45
+ id: 'work-finder',
46
+ name: 'Work Finder',
47
+ port: Number(process.env.WF_PORT) || 5544,
48
+ env: (dir, port) => ({ WF_DATA_DIR: dir, WF_PORT: String(port) }),
49
+ },
50
+ {
51
+ id: 'travel',
52
+ name: 'Travel Assistant',
53
+ port: Number(process.env.TA_PORT) || 5545,
54
+ env: (dir, port) => ({ TA_DB: join(dir, 'db.json'), TA_PORT: String(port) }),
55
+ },
56
+ {
57
+ id: 'client-finder',
58
+ name: 'Client Finder',
59
+ port: Number(process.env.CF_PORT) || 5546,
60
+ env: (dir, port) => ({ CF_DATA_DIR: dir, PORT: String(port) }),
61
+ },
62
+ ];
63
+
64
+ function hasClaude() {
65
+ try {
66
+ return spawnSync('claude', ['--version'], { stdio: 'ignore', shell: isWin, timeout: 8000 }).status === 0;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
71
+
72
+ function openBrowser(url) {
73
+ try {
74
+ if (platform === 'darwin') spawnSync('open', [url], { stdio: 'ignore' });
75
+ else if (isWin) spawnSync('cmd', ['/c', 'start', '""', url], { stdio: 'ignore' });
76
+ else spawnSync('xdg-open', [url], { stdio: 'ignore' });
77
+ } catch {
78
+ /* headless box — the printed URL is the fallback */
79
+ }
80
+ }
81
+
82
+ /** Resolves once the port answers at all — each backend has its own health path. */
83
+ async function waitFor(port, tries = 40) {
84
+ for (let i = 0; i < tries; i++) {
85
+ try {
86
+ const r = await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(1200) });
87
+ if (r) return true;
88
+ } catch {
89
+ /* not up yet */
90
+ }
91
+ await new Promise((r) => setTimeout(r, 500));
92
+ }
93
+ return false;
94
+ }
95
+
96
+ /* ══ Off switch & first aid ═══════════════════════════════════════════════════
97
+ A tool that can only be started is a trap: the person who installed it must be
98
+ able to stop it, see whether it is alive, and fix it when it is not — without
99
+ knowing what a process is. Hence `stop` / `status` / `restart` / `doctor`, and
100
+ the same words printed on the tool's own setup screen. */
101
+
102
+ /** PIDs listening on a port, on any platform. Empty when nothing holds it. */
103
+ function pidsOn(port) {
104
+ try {
105
+ if (isWin) {
106
+ const out = spawnSync('netstat', ['-ano'], { encoding: 'utf8', timeout: 6000 }).stdout || '';
107
+ return [
108
+ ...new Set(
109
+ out
110
+ .split('\n')
111
+ .filter((l) => l.includes(`:${port}`) && /LISTENING/i.test(l))
112
+ .map((l) => l.trim().split(/\s+/).pop())
113
+ .filter((p) => p && p !== '0')
114
+ ),
115
+ ];
116
+ }
117
+ const out = spawnSync('lsof', ['-ti', `tcp:${port}`, '-sTCP:LISTEN'], { encoding: 'utf8', timeout: 6000 }).stdout || '';
118
+ return out.split('\n').map((s) => s.trim()).filter(Boolean);
119
+ } catch {
120
+ return [];
121
+ }
122
+ }
123
+
124
+ function killPid(pid) {
125
+ try {
126
+ if (isWin) spawnSync('taskkill', ['/PID', String(pid), '/F'], { stdio: 'ignore', timeout: 6000 });
127
+ else process.kill(Number(pid), 'SIGTERM');
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
132
+ }
133
+
134
+ async function isUp(port) {
135
+ try {
136
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(1200) });
137
+ return true;
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
142
+
143
+ async function cmdStatus() {
144
+ console.log(`\n${c.blue(c.b(' ◆ Qiksy'))}${c.dim(' — status')}\n`);
145
+ let anyUp = false;
146
+ for (const t of TOOLS) {
147
+ const up = await isUp(t.port);
148
+ anyUp ||= up;
149
+ console.log(up ? c.green(` ✓ ${t.name.padEnd(18)} running · http://localhost:${t.port}`) : c.dim(` · ${t.name.padEnd(18)} not running`));
150
+ }
151
+ console.log(
152
+ anyUp
153
+ ? c.dim('\n Stop with: npx qiksy stop\n')
154
+ : c.dim('\n Start with: npx qiksy\n')
155
+ );
156
+ }
157
+
158
+ function cmdStop() {
159
+ console.log(`\n${c.blue(c.b(' ◆ Qiksy'))}${c.dim(' — stopping')}\n`);
160
+ let killed = 0;
161
+ for (const t of TOOLS) {
162
+ const pids = pidsOn(t.port);
163
+ if (!pids.length) {
164
+ console.log(c.dim(` · ${t.name.padEnd(18)} was not running`));
165
+ continue;
166
+ }
167
+ for (const p of pids) if (killPid(p)) killed++;
168
+ console.log(c.green(` ✓ ${t.name.padEnd(18)} stopped`));
169
+ }
170
+ console.log(c.dim(`\n ${killed ? 'Everything is off.' : 'Nothing was running.'} Start again: npx qiksy\n`));
171
+ }
172
+
173
+ async function cmdDoctor() {
174
+ console.log(`\n${c.blue(c.b(' ◆ Qiksy'))}${c.dim(' — checkup')}\n`);
175
+ const major = Number(process.versions.node.split('.')[0]);
176
+ console.log(major >= 18 ? c.green(` ✓ Node.js ${process.versions.node}`) : c.red(` ✗ Node.js ${process.versions.node} — needs 18 or newer`));
177
+ console.log(hasClaude() ? c.green(' ✓ Claude Code found') : c.amber(' ! Claude Code not found — install it from https://claude.com/claude-code'));
178
+ console.log(existsSync(DATA_HOME) ? c.green(` ✓ Your data folder: ${DATA_HOME}`) : c.amber(` ! No data folder yet (created on first run): ${DATA_HOME}`));
179
+
180
+ for (const t of TOOLS) {
181
+ const bundled = existsSync(join(PKG, 'tools', t.id, 'vendor', 'server.mjs'));
182
+ const up = await isUp(t.port);
183
+ const held = pidsOn(t.port);
184
+ if (!bundled) console.log(c.dim(` · ${t.name.padEnd(18)} not in this build`));
185
+ else if (up) console.log(c.green(` ✓ ${t.name.padEnd(18)} answering on ${t.port}`));
186
+ else if (held.length) console.log(c.amber(` ! ${t.name.padEnd(18)} port ${t.port} is taken by something else (pid ${held.join(', ')})`));
187
+ else console.log(c.dim(` · ${t.name.padEnd(18)} not running`));
188
+ }
189
+
190
+ console.log(c.dim('\n If something is stuck: npx qiksy restart'));
191
+ console.log(c.dim(' Your work is kept in the data folder above — restarting never deletes it.\n'));
192
+ }
193
+
194
+ const children = [];
195
+
196
+ function start(tool) {
197
+ const dir0 = join(PKG, 'tools', tool.id, 'vendor');
198
+ const entry = join(dir0, 'server.mjs');
199
+ if (!existsSync(entry)) return null;
200
+ const dataDir = join(DATA_HOME, tool.id);
201
+ try {
202
+ mkdirSync(dataDir, { recursive: true });
203
+ } catch {
204
+ /* ignore */
205
+ }
206
+ const child = spawn(process.execPath, [entry], {
207
+ cwd: dir0,
208
+ env: { ...process.env, ...tool.env(dataDir, tool.port) },
209
+ stdio: ['ignore', 'pipe', 'pipe'],
210
+ });
211
+ // The backends are chatty; their logs belong in the engine's window, prefixed so a
212
+ // three-way tail stays readable.
213
+ const tag = c.dim(`[${tool.id}] `);
214
+ child.stdout?.on('data', (d) => process.stdout.write(String(d).replace(/^/gm, tag)));
215
+ child.stderr?.on('data', (d) => process.stderr.write(String(d).replace(/^/gm, tag)));
216
+ child.on('exit', (code) => {
217
+ if (code && code !== 0) console.log(c.red(` ✗ ${tool.name} stopped (exit ${code})`));
218
+ });
219
+ children.push(child);
220
+ return child;
221
+ }
222
+
223
+ function stopAll() {
224
+ for (const ch of children) {
225
+ try {
226
+ ch.kill('SIGTERM');
227
+ } catch {
228
+ /* already gone */
229
+ }
230
+ }
231
+ }
232
+ process.on('SIGINT', () => {
233
+ console.log(c.dim('\n Stopping…'));
234
+ stopAll();
235
+ process.exit(0);
236
+ });
237
+ process.on('SIGTERM', () => {
238
+ stopAll();
239
+ process.exit(0);
240
+ });
241
+
242
+ (async () => {
243
+ const cmd = (process.argv[2] || '').toLowerCase();
244
+
245
+ if (cmd === 'status') return cmdStatus();
246
+ if (cmd === 'stop') return cmdStop();
247
+ if (cmd === 'doctor' || cmd === 'check') return cmdDoctor();
248
+ if (cmd === 'help' || cmd === '--help' || cmd === '-h') {
249
+ console.log(`\n${c.blue(c.b(' ◆ Qiksy'))}${c.dim(' — your local engine')}\n`);
250
+ console.log(' npx qiksy start everything (keep the window open)');
251
+ console.log(' npx qiksy status what is running right now');
252
+ console.log(' npx qiksy stop turn it off');
253
+ console.log(' npx qiksy restart turn it off and on again');
254
+ console.log(' npx qiksy doctor check what is wrong\n');
255
+ return;
256
+ }
257
+ if (cmd === 'restart') {
258
+ cmdStop();
259
+ // Give the OS a moment to release the ports before we bind them again.
260
+ await new Promise((r) => setTimeout(r, 1200));
261
+ }
262
+
263
+ console.log(`\n${c.blue(c.b(' ◆ Qiksy'))}${c.dim(' — your local engine')}`);
264
+ console.log(c.dim(' Runs on your own Claude · nothing leaves your machine\n'));
265
+
266
+ if (hasClaude()) {
267
+ console.log(c.green(' ✓ Claude Code detected\n'));
268
+ } else {
269
+ console.log(c.amber(' ! Claude Code not found on PATH'));
270
+ console.log(c.dim(' The tools still open and show you how to connect it.\n'));
271
+ }
272
+
273
+ const started = [];
274
+ for (const tool of TOOLS) {
275
+ const ch = start(tool);
276
+ if (ch) started.push(tool);
277
+ else console.log(c.dim(` – ${tool.name} is not bundled in this build`));
278
+ }
279
+
280
+ if (!started.length) {
281
+ console.log(c.red(' ✗ Nothing to start — this package looks incomplete.'));
282
+ process.exit(1);
283
+ }
284
+
285
+ console.log(c.dim(` Starting ${started.length} tool${started.length > 1 ? 's' : ''}…\n`));
286
+ const up = [];
287
+ for (const tool of started) {
288
+ const ok = await waitFor(tool.port);
289
+ console.log(
290
+ ok
291
+ ? c.green(` ✓ ${tool.name.padEnd(18)} http://localhost:${tool.port}`)
292
+ : c.amber(` ! ${tool.name.padEnd(18)} did not answer yet`)
293
+ );
294
+ if (ok) up.push(tool);
295
+ }
296
+
297
+ console.log('');
298
+ console.log(c.b(' Everything is running. Go back to your Qiksy tab — it connects on its own.'));
299
+ console.log(c.dim(' Keep this window open while you work. Ctrl+C to stop.\n'));
300
+
301
+ if (up.length && !process.env.QIKSY_NO_OPEN) openBrowser('https://qiksy.app/');
302
+ })();
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "qiksy",
3
+ "version": "1.0.0",
4
+ "description": "Qiksy — one local engine for every Qiksy tool (Work Finder, Travel Assistant, Client Finder). Runs on your own Claude; nothing leaves your machine.",
5
+ "type": "module",
6
+ "bin": {
7
+ "qiksy": "bin/qiksy.mjs"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/qiksy.mjs",
11
+ "bundle": "node scripts/bundle.mjs"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "tools",
16
+ "ARCHITECTURE.md",
17
+ "INSTALL-PROMPT.md",
18
+ "README.md"
19
+ ],
20
+ "keywords": [
21
+ "qiksy",
22
+ "claude",
23
+ "local-first",
24
+ "job-search",
25
+ "agents"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "license": "UNLICENSED",
31
+ "homepage": "https://qiksy.app/"
32
+ }
@@ -0,0 +1,70 @@
1
+ # Client Finder — движок, этап 0
2
+
3
+ **Критерии на входе → список покупателей с доказательствами на выходе.**
4
+ Ни интерфейса, ни рассылки, ни браузера — только та часть, которая решает,
5
+ существует ли продукт вообще. Полный план: [docs/CLIENT-FINDER.md](../../docs/CLIENT-FINDER.md).
6
+
7
+ ## Запуск
8
+
9
+ ```bash
10
+ npm run find:clients products/client-finder-backend/icp/assist-ua-shops.json
11
+ # или из этой папки:
12
+ node find.mjs icp/assist-ua-shops.json --count 20 --model sonnet
13
+ ```
14
+
15
+ Флаги: `--count` (сколько кандидатов, по умолчанию 12), `--model` (`sonnet` /
16
+ `opus`), `--no-verify` (пропустить проверку ссылок).
17
+
18
+ Результат — в `runs/`: `.json` для следующей стадии продукта и `.md`, который
19
+ читает человек. Токены идут по **твоей** подписке Claude через CLI `claude`;
20
+ прогон печатает, сколько потратил.
21
+
22
+ ## Критерий этапа 0
23
+
24
+ Он назван заранее, чтобы «эффективно» потом не стало ощущением:
25
+
26
+ 1. Из 20 найденных контактов ты сам признаёшь годными **≥ 12** — «этим я бы написал».
27
+ 2. На эти 20 уходит **меньше времени**, чем на те же 20 руками.
28
+ 3. Через инструмент приходит **хотя бы одна реальная сделка студии**.
29
+
30
+ Не выполнилось — продукта нет, и мы не построили под него витрину.
31
+
32
+ ## Как это устроено
33
+
34
+ | Файл | Что делает |
35
+ |---|---|
36
+ | [find.mjs](find.mjs) | CLI: читает ICP, гоняет агента, проверяет, пишет отчёт |
37
+ | [lib/agent.mjs](lib/agent.mjs) | запускает `claude -p` со stream-json; кандидаты стримятся строками `PROSPECT: {…}` по мере находки |
38
+ | [lib/approver-mcp.mjs](lib/approver-mcp.mjs) | MCP-разрешалка: в `-p` режиме глобальный «ask» превращается в «deny» и глушит WebSearch/WebFetch — этот сервер их пропускает, а всё остальное (Bash, файлы) оставляет запрещённым |
39
+ | [lib/prompts.mjs](lib/prompts.mjs) | **промпт и есть продукт** — контракт доказательств живёт здесь |
40
+ | [lib/verify.mjs](lib/verify.mjs) | независимо дёргает каждую ссылку: агент сказал «я открыл» — проверяем, что страница есть |
41
+ | [lib/report.mjs](lib/report.mjs) | markdown, который читаешь ты: зацепка, доказательства, честные пометки о мёртвых ссылках |
42
+ | `icp/*.json` | сами критерии |
43
+
44
+ ## Почему без браузера
45
+
46
+ Этап 0 ищет по публичному вебу — поиск и сайты компаний, этого достаточно.
47
+ Playwright и залогиненные сессии означали бы ToS социальных сетей в продукте,
48
+ которого ещё нет. Форма «агент находит и доказывает → пишет человек» появится
49
+ на этапе 2, и появится осознанно.
50
+
51
+ ## Как писать ICP
52
+
53
+ «Продай Assist» — не задача. Задача выглядит так: *владельцы магазинов на
54
+ Shopify в Польше, у которых на сайте нет чата, писать по-польски, зацепка —
55
+ конкретная страница их сайта*. Поля:
56
+
57
+ - `who` — одно предложение, кто это;
58
+ - `must` — критерии, каждый из которых агент обязан **проверить на странице**;
59
+ - `mustNot` — то, что дисквалифицирует (и агент обязан сказать, что проверил);
60
+ - `signals` — не обязательные признаки, поднимают уверенность;
61
+ - `geo`, `language`, `contact`, `avoid`.
62
+
63
+ Чем конкретнее `must`, тем меньше мусора: критерий, который нельзя проверить
64
+ на странице, агент проверить не сможет — и либо соврёт, либо выкинет кандидата.
65
+
66
+ ## Персональные данные
67
+
68
+ `runs/` **не коммитится** — там имена, почты и сайты живых людей. Хранится
69
+ локально, у тебя. Когда дойдёт до продукта, срок хранения и право на удаление
70
+ надо будет прописать (см. открытые вопросы в docs/CLIENT-FINDER.md).
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Client Finder — stage 0: criteria in, a checkable list of buyers out.
4
+ *
5
+ * node find.mjs icp/assist-shopify-pl.json # 12 prospects (default)
6
+ * node find.mjs icp/assist-shopify-pl.json --count 20
7
+ * node find.mjs icp/… --model opus --no-verify
8
+ *
9
+ * No UI, no outreach, no browser — the whole point of this stage is to learn
10
+ * whether the engine can find people the owner would actually write to. Everything
11
+ * else waits until it can (see docs/CLIENT-FINDER.md).
12
+ *
13
+ * Costs run on YOUR Claude subscription/key via the `claude` CLI; the run prints
14
+ * what it spent.
15
+ */
16
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
17
+ import { resolve, dirname, basename } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { runClaude, extractJson } from './lib/agent.mjs';
20
+ import { researchPrompt } from './lib/prompts.mjs';
21
+ import { verifyAll } from './lib/verify.mjs';
22
+ import { toMarkdown } from './lib/report.mjs';
23
+
24
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)));
25
+
26
+ /* ── args ─────────────────────────────────────────────────────────────────── */
27
+ const argv = process.argv.slice(2);
28
+ const flag = (name, fallback) => {
29
+ const i = argv.indexOf(`--${name}`);
30
+ return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : fallback;
31
+ };
32
+ const has = (name) => argv.includes(`--${name}`);
33
+
34
+ const icpPath = argv.find((a) => !a.startsWith('--'));
35
+ if (!icpPath) {
36
+ console.error('Usage: node find.mjs <icp.json> [--count 12] [--model sonnet] [--no-verify]');
37
+ process.exit(1);
38
+ }
39
+ const count = Number(flag('count', 12));
40
+ const model = flag('model', 'sonnet');
41
+
42
+ // Accept the path as typed: from the repo root (`npm run find:clients -- products/…`)
43
+ // or from this folder (`node find.mjs icp/…`). Both are natural; failing on one of
44
+ // them is just a papercut in the way of the first run.
45
+ const icpFile = [resolve(process.cwd(), icpPath), resolve(ROOT, icpPath)].find(existsSync);
46
+ if (!icpFile) {
47
+ console.error(`✗ ICP not found: ${icpPath} (looked in ${process.cwd()} and ${ROOT})`);
48
+ process.exit(1);
49
+ }
50
+ /** @type {import('./lib/prompts.mjs').Icp} */
51
+ const icp = JSON.parse(readFileSync(icpFile, 'utf8'));
52
+
53
+ /* ── run ──────────────────────────────────────────────────────────────────── */
54
+ const started = new Date();
55
+ console.log(`\n🎯 ${icp.product.name} · ICP «${icp.id}» · цель: ${count} кандидатов · модель ${model}`);
56
+ console.log(` ${icp.who}\n`);
57
+
58
+ const streamed = [];
59
+ const t0 = Date.now();
60
+
61
+ const res = await runClaude({
62
+ prompt: researchPrompt(icp, count),
63
+ model,
64
+ onEvent: (line) => process.stdout.write(` ${line}\n`),
65
+ onPartial: (p) => {
66
+ streamed.push(p);
67
+ console.log(`\n✅ ${streamed.length}. ${p.company} — ${p.url}`);
68
+ if (p.hook) console.log(` зацепка: ${p.hook}`);
69
+ },
70
+ });
71
+
72
+ if (!res.ok) {
73
+ console.error(`\n✗ агент упал: ${res.error}`);
74
+ process.exit(1);
75
+ }
76
+
77
+ // The final fenced array is authoritative (a streamed line can be malformed); fall
78
+ // back to what streamed if the agent forgot the closing array.
79
+ const parsed = extractJson(res.result);
80
+ let prospects = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.prospects) ? parsed.prospects : streamed;
81
+ if (!prospects.length) prospects = streamed;
82
+
83
+ console.log(`\n📋 агент вернул ${prospects.length} кандидат(ов) за ${Math.round(res.durationMs / 1000)}с${res.costUsd != null ? ` · $${res.costUsd.toFixed(2)}` : ''}`);
84
+
85
+ /* ── verify ───────────────────────────────────────────────────────────────── */
86
+ if (!has('no-verify')) {
87
+ console.log(`\n🔎 проверяю ссылки (сайт + каждый источник)…`);
88
+ prospects = await verifyAll(prospects, (p) => {
89
+ const v = p.verified;
90
+ const mark = !v.siteAlive ? '🔴' : v.dead.length ? '🟠' : v.blocked.length ? '🟡' : '🟢';
91
+ console.log(` ${mark} ${p.company} — проверено ${v.checked}${v.dead.length ? `, мёртвых ${v.dead.length}` : ''}`);
92
+ });
93
+ }
94
+
95
+ /* ── write ────────────────────────────────────────────────────────────────── */
96
+ const meta = {
97
+ icp: icp.id,
98
+ model,
99
+ startedAt: started.toISOString(),
100
+ finishedAt: new Date().toISOString(),
101
+ durationMs: Date.now() - t0,
102
+ costUsd: res.costUsd,
103
+ requested: count,
104
+ returned: prospects.length,
105
+ notes: String(res.result || '')
106
+ .replace(/```json[\s\S]*?```/g, '')
107
+ .trim()
108
+ .slice(-1200),
109
+ };
110
+
111
+ const runsDir = resolve(ROOT, 'runs');
112
+ mkdirSync(runsDir, { recursive: true });
113
+ const stamp = started.toISOString().replace(/[:.]/g, '-').slice(0, 19);
114
+ const slug = `${stamp}-${basename(icpPath).replace(/\.json$/, '')}`;
115
+ writeFileSync(resolve(runsDir, `${slug}.json`), JSON.stringify({ icp, meta, prospects }, null, 2));
116
+ writeFileSync(resolve(runsDir, `${slug}.md`), toMarkdown({ icp, prospects, meta }));
117
+
118
+ const clean = prospects.filter((p) => p.verified?.ok !== false).length;
119
+ console.log(`\n📄 runs/${slug}.md`);
120
+ console.log(` ${prospects.length} кандидат(ов), из них ${clean} прошли проверку ссылок`);
121
+ console.log(`\nКритерий этапа 0: скольким из них ты бы написал? Нужно ≥ 12 из 20.\n`);