open-classify 0.6.0 → 0.8.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/README.md +58 -47
- package/bin/open-classify.mjs +657 -0
- package/dist/src/classifiers.d.ts +12 -5
- package/dist/src/classifiers.js +32 -16
- package/dist/src/classify.d.ts +4 -1
- package/dist/src/classify.js +28 -6
- package/dist/src/config.d.ts +1 -1
- package/dist/src/config.js +0 -5
- package/dist/src/ollama.d.ts +5 -6
- package/dist/src/ollama.js +17 -11
- package/dist/src/pipeline.d.ts +3 -1
- package/dist/src/pipeline.js +15 -10
- package/docs/adding-a-classifier.md +46 -25
- package/open-classify.config.example.json +1 -3
- package/package.json +6 -1
- /package/{dist/src/classifiers → templates}/context_shift/manifest.json +0 -0
- /package/{dist/src/classifiers → templates}/context_shift/prompt.md +0 -0
- /package/{dist/src/classifiers → templates}/conversation_digest/manifest.json +0 -0
- /package/{dist/src/classifiers → templates}/conversation_digest/prompt.md +0 -0
- /package/{dist/src/classifiers → templates}/memory_retrieval_queries/manifest.json +0 -0
- /package/{dist/src/classifiers → templates}/memory_retrieval_queries/prompt.md +0 -0
- /package/{dist/src/classifiers → templates}/tools/manifest.json +0 -0
- /package/{dist/src/classifiers → templates}/tools/prompt.md +0 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// open-classify CLI. Subcommands: init, doctor, try.
|
|
3
|
+
//
|
|
4
|
+
// init: scaffold the standard project layout for a consumer.
|
|
5
|
+
// doctor: verify the install, config, Ollama, and classifiers are all working.
|
|
6
|
+
// try: run the pipeline against a single message and print the result.
|
|
7
|
+
|
|
8
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { createInterface } from "node:readline";
|
|
10
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
|
|
14
|
+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const PACKAGE_ROOT = resolve(SCRIPT_DIR, "..");
|
|
16
|
+
const TEMPLATES_DIR = join(PACKAGE_ROOT, "templates");
|
|
17
|
+
|
|
18
|
+
const TEMPLATE_NAMES = ["conversation_digest", "context_shift", "memory_retrieval_queries", "tools"];
|
|
19
|
+
|
|
20
|
+
const TEMPLATE_DESCRIPTIONS = {
|
|
21
|
+
conversation_digest: "rolling summary of recent turns",
|
|
22
|
+
context_shift: "detects topic changes",
|
|
23
|
+
memory_retrieval_queries: "generates queries for a memory store",
|
|
24
|
+
tools: "tool-call routing",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const CLASSIFIERS_README = `# classifiers/
|
|
28
|
+
|
|
29
|
+
Drop a folder here per classifier. Each folder needs:
|
|
30
|
+
|
|
31
|
+
- \`manifest.json\` — see [open-classify docs](https://github.com/taylorbayouth/open-classify/blob/main/docs/adding-a-classifier.md)
|
|
32
|
+
- \`prompt.md\` — the classifier-specific instructions
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
\`\`\`js
|
|
37
|
+
import { createClassifier } from "open-classify";
|
|
38
|
+
|
|
39
|
+
const { classify } = createClassifier({
|
|
40
|
+
extraClassifierDirs: ["./classifiers"],
|
|
41
|
+
});
|
|
42
|
+
\`\`\`
|
|
43
|
+
|
|
44
|
+
Place this in your server entry point. Call \`classify(input)\` for each user message.
|
|
45
|
+
\`extraClassifierDirs\` is resolved relative to the current working directory.
|
|
46
|
+
|
|
47
|
+
## Activating templates
|
|
48
|
+
|
|
49
|
+
The four \`_<name>/\` directories below are templates copied from the package — they ship inactive (the loader skips any folder starting with \`_\`). Activate one by dropping the underscore:
|
|
50
|
+
|
|
51
|
+
\`\`\`sh
|
|
52
|
+
mv _tools tools
|
|
53
|
+
\`\`\`
|
|
54
|
+
|
|
55
|
+
You probably also want to edit its \`manifest.json\` first to fit your app (e.g. trim the \`allowed_tools\` list).
|
|
56
|
+
|
|
57
|
+
## Deactivating without deleting
|
|
58
|
+
|
|
59
|
+
Same trick in reverse — rename \`my_classifier\` → \`_my_classifier\` to take it out of the active set without losing your work.
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const DEFAULT_CONFIG = {
|
|
63
|
+
runner: {
|
|
64
|
+
provider: "ollama",
|
|
65
|
+
host: "http://127.0.0.1:11434",
|
|
66
|
+
defaultModel: "gemma4:e4b-it-q4_K_M",
|
|
67
|
+
},
|
|
68
|
+
catalog: "downstream-models.json",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Entry point
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
async function main() {
|
|
76
|
+
const args = process.argv.slice(2);
|
|
77
|
+
const subcommand = args[0];
|
|
78
|
+
|
|
79
|
+
if (!subcommand || subcommand === "-h" || subcommand === "--help") {
|
|
80
|
+
printHelp();
|
|
81
|
+
process.exit(subcommand ? 0 : 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (subcommand === "init") {
|
|
85
|
+
const flags = parseInitFlags(args.slice(1));
|
|
86
|
+
await runInit({ cwd: process.cwd(), ...flags });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (subcommand === "doctor") {
|
|
91
|
+
await runDoctor({ cwd: process.cwd() });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (subcommand === "try") {
|
|
96
|
+
const message = args.slice(1).join(" ");
|
|
97
|
+
await runTry({ cwd: process.cwd(), message });
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
102
|
+
printHelp();
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Shared helpers
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
function parseInitFlags(args) {
|
|
111
|
+
const flags = {
|
|
112
|
+
yes: false,
|
|
113
|
+
minimal: false,
|
|
114
|
+
dryRun: false,
|
|
115
|
+
force: false,
|
|
116
|
+
noInstall: false,
|
|
117
|
+
packageManager: null,
|
|
118
|
+
classifierDir: "classifiers",
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < args.length; i++) {
|
|
122
|
+
const arg = args[i];
|
|
123
|
+
if (arg === "--yes" || arg === "-y") flags.yes = true;
|
|
124
|
+
else if (arg === "--minimal") flags.minimal = true;
|
|
125
|
+
else if (arg === "--dry-run") flags.dryRun = true;
|
|
126
|
+
else if (arg === "--force") flags.force = true;
|
|
127
|
+
else if (arg === "--no-install") flags.noInstall = true;
|
|
128
|
+
else if (arg === "--package-manager" && args[i + 1]) flags.packageManager = args[++i];
|
|
129
|
+
else if (arg.startsWith("--package-manager=")) flags.packageManager = arg.split("=")[1];
|
|
130
|
+
else if (arg === "--classifier-dir" && args[i + 1]) flags.classifierDir = args[++i];
|
|
131
|
+
else if (arg.startsWith("--classifier-dir=")) flags.classifierDir = arg.split("=")[1];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return flags;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function printHelp() {
|
|
138
|
+
process.stdout.write(`open-classify — runtime CLI
|
|
139
|
+
|
|
140
|
+
Commands:
|
|
141
|
+
init [options] Scaffold open-classify.config.json and classifiers/ in the
|
|
142
|
+
current directory. Re-run safe: existing files are skipped.
|
|
143
|
+
|
|
144
|
+
doctor Check that the install, config, Ollama, and classifiers are
|
|
145
|
+
all working. Exits non-zero on failure.
|
|
146
|
+
|
|
147
|
+
try <message> Run the pipeline against a single message and print the
|
|
148
|
+
result. Useful for verifying your setup without touching
|
|
149
|
+
application code.
|
|
150
|
+
|
|
151
|
+
Options for init:
|
|
152
|
+
--minimal Write only open-classify.config.json; skip classifiers/
|
|
153
|
+
--dry-run Preview what would be created; don't write anything
|
|
154
|
+
--force Overwrite existing files without prompting
|
|
155
|
+
--no-install Skip the "add to package.json" prompt
|
|
156
|
+
--package-manager <m> npm | pnpm | yarn | bun (default: auto-detect)
|
|
157
|
+
--classifier-dir <p> Directory for classifiers (default: ./classifiers)
|
|
158
|
+
--yes, -y Accept all prompts (CI mode)
|
|
159
|
+
|
|
160
|
+
`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function detectPackageManager(cwd) {
|
|
164
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
165
|
+
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
166
|
+
if (existsSync(join(cwd, "bun.lockb"))) return "bun";
|
|
167
|
+
return "npm";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function isOpenClassifyDep(pkg) {
|
|
171
|
+
return ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"].some(
|
|
172
|
+
(f) => pkg[f]?.["open-classify"],
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getCliVersion() {
|
|
177
|
+
try {
|
|
178
|
+
return JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")).version;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// init
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
async function runInit({ cwd, yes, minimal, dryRun, force, noInstall, packageManager, classifierDir }) {
|
|
189
|
+
// 1. Preflight: require a host project.
|
|
190
|
+
const pkgPath = join(cwd, "package.json");
|
|
191
|
+
if (!existsSync(pkgPath)) {
|
|
192
|
+
process.stderr.write(
|
|
193
|
+
`✖ No package.json found in ${cwd}.\n` +
|
|
194
|
+
` open-classify scaffolds code that imports the library, so it needs a\n` +
|
|
195
|
+
` Node project to live in.\n\n` +
|
|
196
|
+
` Create one first: npm init -y\n`,
|
|
197
|
+
);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let pkg;
|
|
202
|
+
try {
|
|
203
|
+
pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
204
|
+
} catch {
|
|
205
|
+
process.stderr.write(`✖ Could not parse package.json at ${pkgPath}\n`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 2. Offer to install if not yet a dependency (skip in --yes / --no-install mode).
|
|
210
|
+
let installedNow = false;
|
|
211
|
+
if (!isOpenClassifyDep(pkg) && !noInstall && !yes) {
|
|
212
|
+
process.stdout.write(`ℹ open-classify is not yet a dependency of this project.\n\n`);
|
|
213
|
+
const doInstall = await confirm("? Add open-classify to package.json and install it now? (Y/n) ", true);
|
|
214
|
+
if (doInstall) {
|
|
215
|
+
const pm = packageManager || detectPackageManager(cwd);
|
|
216
|
+
const installCmd = pm === "npm" ? ["install", "open-classify"] : ["add", "open-classify"];
|
|
217
|
+
process.stdout.write(`\n Running: ${pm} ${installCmd.join(" ")}\n\n`);
|
|
218
|
+
const result = spawnSync(pm, installCmd, { cwd, stdio: "inherit" });
|
|
219
|
+
if (result.status !== 0) {
|
|
220
|
+
process.stderr.write(`\n✖ Install failed. Run manually: ${pm} ${installCmd.join(" ")}\n`);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
installedNow = true;
|
|
224
|
+
process.stdout.write("\n");
|
|
225
|
+
} else {
|
|
226
|
+
process.stdout.write(
|
|
227
|
+
` Skipped. You'll need to run \`npm install open-classify\` before importing.\n\n`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 3. Plan.
|
|
233
|
+
const resolvedClassifierDir = resolve(cwd, classifierDir);
|
|
234
|
+
const wrote = { config: false, readme: false, templateCount: 0 };
|
|
235
|
+
let plan = planInit(cwd, { minimal, classifierDir: resolvedClassifierDir, force, wrote });
|
|
236
|
+
|
|
237
|
+
// Nothing to do.
|
|
238
|
+
if (plan.toCreate.length === 0) {
|
|
239
|
+
process.stdout.write("Nothing to do — your project already has all the scaffolded files.\n");
|
|
240
|
+
if (plan.toSkip.length > 0) {
|
|
241
|
+
process.stdout.write("\nAlready in place:\n");
|
|
242
|
+
for (const p of plan.toSkip) process.stdout.write(` ${p}\n`);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 4. Preview.
|
|
248
|
+
process.stdout.write(`\nThe following will be created in ${cwd}:\n\n`);
|
|
249
|
+
for (const item of plan.preview) {
|
|
250
|
+
if (item.isGroupHeader) {
|
|
251
|
+
process.stdout.write(` ${item.label}\n`);
|
|
252
|
+
} else if (item.indent) {
|
|
253
|
+
process.stdout.write(` ${item.label.padEnd(32)} ${item.description}\n`);
|
|
254
|
+
} else {
|
|
255
|
+
process.stdout.write(` ${item.label.padEnd(34)} ${item.description}\n`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (plan.toSkip.length > 0) {
|
|
260
|
+
process.stdout.write(`\n⚠ These files already exist and will be skipped:\n`);
|
|
261
|
+
for (const p of plan.toSkip) process.stdout.write(` ${p}\n`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 5. Stop here on --dry-run.
|
|
265
|
+
if (dryRun) {
|
|
266
|
+
process.stdout.write("\n(dry run — nothing written)\n");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 6. Conflict handling: interactive only (not --yes, not --force).
|
|
271
|
+
if (plan.toSkip.length > 0 && !yes && !force) {
|
|
272
|
+
const choice = await promptConflict();
|
|
273
|
+
if (choice === "diff") {
|
|
274
|
+
showDiffs(plan.toSkip, cwd, resolvedClassifierDir);
|
|
275
|
+
const choice2 = await promptConflict();
|
|
276
|
+
if (choice2 === "y") {
|
|
277
|
+
plan = planInit(cwd, { minimal, classifierDir: resolvedClassifierDir, force: true, wrote });
|
|
278
|
+
}
|
|
279
|
+
} else if (choice === "y") {
|
|
280
|
+
plan = planInit(cwd, { minimal, classifierDir: resolvedClassifierDir, force: true, wrote });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// 7. Confirm (skip in --yes mode).
|
|
285
|
+
if (!yes) {
|
|
286
|
+
const proceed = await confirm("\n? Continue? (Y/n) ", true);
|
|
287
|
+
if (!proceed) {
|
|
288
|
+
process.stdout.write("Aborted.\n");
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 8. Execute.
|
|
294
|
+
process.stdout.write("\n");
|
|
295
|
+
for (const action of plan.actions) action();
|
|
296
|
+
|
|
297
|
+
// 9. Summary + next steps.
|
|
298
|
+
process.stdout.write("\n");
|
|
299
|
+
if (installedNow) {
|
|
300
|
+
const v = getCliVersion();
|
|
301
|
+
process.stdout.write(`✓ open-classify installed${v ? ` (v${v})` : ""}\n`);
|
|
302
|
+
}
|
|
303
|
+
if (wrote.config) process.stdout.write("✓ wrote open-classify.config.json\n");
|
|
304
|
+
if (wrote.readme || wrote.templateCount > 0) {
|
|
305
|
+
const classifierDirRel = relative(cwd, resolvedClassifierDir);
|
|
306
|
+
if (wrote.templateCount > 0) {
|
|
307
|
+
process.stdout.write(`✓ scaffolded ${wrote.templateCount} classifier(s) in ./${classifierDirRel}/\n`);
|
|
308
|
+
} else {
|
|
309
|
+
process.stdout.write(`✓ wrote ./${classifierDirRel}/README.md\n`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const classifierDirRel = relative(cwd, resolvedClassifierDir);
|
|
314
|
+
process.stdout.write(`
|
|
315
|
+
Next steps:
|
|
316
|
+
|
|
317
|
+
1. Pull the default model:
|
|
318
|
+
ollama pull ${DEFAULT_CONFIG.runner.defaultModel}
|
|
319
|
+
|
|
320
|
+
2. Wire it into your server (example for a Node entrypoint):
|
|
321
|
+
see ./${classifierDirRel}/README.md → "Quickstart"
|
|
322
|
+
|
|
323
|
+
3. Verify the install:
|
|
324
|
+
npx open-classify doctor
|
|
325
|
+
|
|
326
|
+
4. Run a one-shot classification against your config:
|
|
327
|
+
npx open-classify try "hello world"
|
|
328
|
+
|
|
329
|
+
Docs: https://github.com/taylorbayouth/open-classify#readme
|
|
330
|
+
`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function planInit(cwd, { minimal = false, classifierDir, force = false, wrote }) {
|
|
334
|
+
const toCreate = [];
|
|
335
|
+
const toSkip = [];
|
|
336
|
+
const actions = [];
|
|
337
|
+
const preview = [];
|
|
338
|
+
|
|
339
|
+
// Config file.
|
|
340
|
+
const configPath = join(cwd, "open-classify.config.json");
|
|
341
|
+
const configRel = relative(cwd, configPath);
|
|
342
|
+
if (existsSync(configPath) && !force) {
|
|
343
|
+
toSkip.push(configRel);
|
|
344
|
+
} else {
|
|
345
|
+
toCreate.push(configRel);
|
|
346
|
+
preview.push({ label: configRel, description: `(default Ollama setup, ${DEFAULT_CONFIG.runner.defaultModel})` });
|
|
347
|
+
actions.push(() => {
|
|
348
|
+
writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2) + "\n");
|
|
349
|
+
process.stdout.write(` wrote ${configRel}\n`);
|
|
350
|
+
wrote.config = true;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!minimal) {
|
|
355
|
+
const classifierPreviewItems = [];
|
|
356
|
+
|
|
357
|
+
// Ensure the directory exists (prerequisite for all classifier actions).
|
|
358
|
+
if (!existsSync(classifierDir)) {
|
|
359
|
+
toCreate.push(`${relative(cwd, classifierDir)}/`);
|
|
360
|
+
actions.push(() => {
|
|
361
|
+
mkdirSync(classifierDir, { recursive: true });
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// README.md.
|
|
366
|
+
const readmePath = join(classifierDir, "README.md");
|
|
367
|
+
const readmeRel = relative(cwd, readmePath);
|
|
368
|
+
if (existsSync(readmePath) && !force) {
|
|
369
|
+
toSkip.push(readmeRel);
|
|
370
|
+
} else {
|
|
371
|
+
toCreate.push(readmeRel);
|
|
372
|
+
classifierPreviewItems.push({ label: "README.md", indent: true, description: "how to author your own classifier" });
|
|
373
|
+
actions.push(() => {
|
|
374
|
+
mkdirSync(classifierDir, { recursive: true });
|
|
375
|
+
writeFileSync(readmePath, CLASSIFIERS_README);
|
|
376
|
+
process.stdout.write(` wrote ${readmeRel}\n`);
|
|
377
|
+
wrote.readme = true;
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Template classifier directories.
|
|
382
|
+
for (const name of TEMPLATE_NAMES) {
|
|
383
|
+
const inactivePath = join(classifierDir, `_${name}`);
|
|
384
|
+
const activePath = join(classifierDir, name);
|
|
385
|
+
const inactiveRel = relative(cwd, inactivePath);
|
|
386
|
+
const activeRel = relative(cwd, activePath);
|
|
387
|
+
|
|
388
|
+
// Never overwrite an activated (user-renamed) template.
|
|
389
|
+
if (existsSync(activePath)) {
|
|
390
|
+
toSkip.push(`${activeRel}/`);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (existsSync(inactivePath) && !force) {
|
|
395
|
+
toSkip.push(`${inactiveRel}/`);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
toCreate.push(`${inactiveRel}/`);
|
|
400
|
+
classifierPreviewItems.push({
|
|
401
|
+
label: `_${name}/`,
|
|
402
|
+
indent: true,
|
|
403
|
+
description: TEMPLATE_DESCRIPTIONS[name],
|
|
404
|
+
});
|
|
405
|
+
actions.push(() => {
|
|
406
|
+
mkdirSync(classifierDir, { recursive: true });
|
|
407
|
+
if (force && existsSync(inactivePath)) {
|
|
408
|
+
rmSync(inactivePath, { recursive: true, force: true });
|
|
409
|
+
}
|
|
410
|
+
cpSync(join(TEMPLATES_DIR, name), inactivePath, { recursive: true });
|
|
411
|
+
process.stdout.write(` wrote ${inactiveRel}/\n`);
|
|
412
|
+
wrote.templateCount++;
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (classifierPreviewItems.length > 0) {
|
|
417
|
+
preview.push({ label: `${relative(cwd, classifierDir)}/`, isGroupHeader: true });
|
|
418
|
+
preview.push(...classifierPreviewItems);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return { toCreate, toSkip, actions, preview };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function showDiffs(conflicts, cwd, classifierDir) {
|
|
426
|
+
for (const p of conflicts) {
|
|
427
|
+
const isDir = p.endsWith("/");
|
|
428
|
+
const relPath = isDir ? p.slice(0, -1) : p;
|
|
429
|
+
const fullPath = join(cwd, relPath);
|
|
430
|
+
|
|
431
|
+
process.stdout.write(`\n--- ${p} ---\n`);
|
|
432
|
+
|
|
433
|
+
if (!isDir) {
|
|
434
|
+
process.stdout.write("\n current:\n");
|
|
435
|
+
try {
|
|
436
|
+
const lines = readFileSync(fullPath, "utf8").split("\n");
|
|
437
|
+
for (const line of lines) process.stdout.write(` ${line}\n`);
|
|
438
|
+
} catch {
|
|
439
|
+
process.stdout.write(" (could not read)\n");
|
|
440
|
+
}
|
|
441
|
+
process.stdout.write("\n would become:\n");
|
|
442
|
+
for (const line of JSON.stringify(DEFAULT_CONFIG, null, 2).split("\n")) {
|
|
443
|
+
process.stdout.write(` ${line}\n`);
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
process.stdout.write("\n current files:\n");
|
|
447
|
+
try {
|
|
448
|
+
for (const f of readdirSync(fullPath)) process.stdout.write(` ${f}\n`);
|
|
449
|
+
} catch {
|
|
450
|
+
process.stdout.write(" (could not read)\n");
|
|
451
|
+
}
|
|
452
|
+
const templateName = basename(relPath).replace(/^_/, "");
|
|
453
|
+
const templatePath = join(TEMPLATES_DIR, templateName);
|
|
454
|
+
if (existsSync(templatePath)) {
|
|
455
|
+
process.stdout.write("\n template files:\n");
|
|
456
|
+
for (const f of readdirSync(templatePath)) process.stdout.write(` ${f}\n`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
process.stdout.write("\n");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ---------------------------------------------------------------------------
|
|
464
|
+
// doctor
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
async function runDoctor({ cwd }) {
|
|
468
|
+
let allGood = true;
|
|
469
|
+
|
|
470
|
+
// 1. package.json + open-classify dep.
|
|
471
|
+
const pkgPath = join(cwd, "package.json");
|
|
472
|
+
if (!existsSync(pkgPath)) {
|
|
473
|
+
process.stdout.write("✖ No package.json — not a Node project\n");
|
|
474
|
+
allGood = false;
|
|
475
|
+
} else {
|
|
476
|
+
let pkg;
|
|
477
|
+
try { pkg = JSON.parse(readFileSync(pkgPath, "utf8")); } catch { pkg = {}; }
|
|
478
|
+
if (isOpenClassifyDep(pkg)) {
|
|
479
|
+
process.stdout.write("✓ open-classify found in package.json\n");
|
|
480
|
+
} else {
|
|
481
|
+
process.stdout.write("⚠ open-classify not listed as a dependency\n");
|
|
482
|
+
allGood = false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 2. Config parses.
|
|
487
|
+
const configPath = join(cwd, "open-classify.config.json");
|
|
488
|
+
if (!existsSync(configPath)) {
|
|
489
|
+
process.stdout.write("✖ No open-classify.config.json — run: npx open-classify init\n");
|
|
490
|
+
allGood = false;
|
|
491
|
+
} else {
|
|
492
|
+
let config;
|
|
493
|
+
try {
|
|
494
|
+
config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
495
|
+
process.stdout.write("✓ open-classify.config.json parses OK\n");
|
|
496
|
+
|
|
497
|
+
// 3. Ollama reachable.
|
|
498
|
+
const host = config.runner?.host || DEFAULT_CONFIG.runner.host;
|
|
499
|
+
try {
|
|
500
|
+
const res = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
|
501
|
+
if (res.ok) {
|
|
502
|
+
process.stdout.write(`✓ Ollama reachable at ${host}\n`);
|
|
503
|
+
|
|
504
|
+
// 4. Default model pulled.
|
|
505
|
+
const data = await res.json();
|
|
506
|
+
const model = config.runner?.defaultModel || DEFAULT_CONFIG.runner.defaultModel;
|
|
507
|
+
const pulled = data.models?.some((m) => m.name === model || m.model === model);
|
|
508
|
+
if (pulled) {
|
|
509
|
+
process.stdout.write(`✓ Model ${model} is available\n`);
|
|
510
|
+
} else {
|
|
511
|
+
process.stdout.write(`✖ Model ${model} not found — run: ollama pull ${model}\n`);
|
|
512
|
+
allGood = false;
|
|
513
|
+
}
|
|
514
|
+
} else {
|
|
515
|
+
process.stdout.write(`✖ Ollama responded ${res.status} at ${host}\n`);
|
|
516
|
+
allGood = false;
|
|
517
|
+
}
|
|
518
|
+
} catch {
|
|
519
|
+
process.stdout.write(`✖ Ollama not reachable at ${host} — is it running?\n`);
|
|
520
|
+
allGood = false;
|
|
521
|
+
}
|
|
522
|
+
} catch {
|
|
523
|
+
process.stdout.write("✖ open-classify.config.json is not valid JSON\n");
|
|
524
|
+
allGood = false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 5. Classifiers directory.
|
|
529
|
+
const classifiersDir = join(cwd, "classifiers");
|
|
530
|
+
if (existsSync(classifiersDir)) {
|
|
531
|
+
let active = 0;
|
|
532
|
+
let bad = 0;
|
|
533
|
+
try {
|
|
534
|
+
for (const entry of readdirSync(classifiersDir, { withFileTypes: true })) {
|
|
535
|
+
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
536
|
+
const dir = join(classifiersDir, entry.name);
|
|
537
|
+
const ok =
|
|
538
|
+
existsSync(join(dir, "manifest.json")) && existsSync(join(dir, "prompt.md"));
|
|
539
|
+
if (ok) active++;
|
|
540
|
+
else {
|
|
541
|
+
process.stdout.write(`✖ classifiers/${entry.name}/ is missing manifest.json or prompt.md\n`);
|
|
542
|
+
bad++;
|
|
543
|
+
allGood = false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
} catch { /* skip */ }
|
|
547
|
+
if (bad === 0) {
|
|
548
|
+
process.stdout.write(
|
|
549
|
+
active > 0
|
|
550
|
+
? `✓ ${active} active classifier(s) in classifiers/\n`
|
|
551
|
+
: "ℹ No active classifiers in classifiers/ (activate a template with: mv _name name)\n",
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
} else {
|
|
555
|
+
process.stdout.write("ℹ No classifiers/ directory — run: npx open-classify init\n");
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (!allGood) process.exit(1);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ---------------------------------------------------------------------------
|
|
562
|
+
// try
|
|
563
|
+
// ---------------------------------------------------------------------------
|
|
564
|
+
|
|
565
|
+
async function runTry({ cwd, message }) {
|
|
566
|
+
if (!message) {
|
|
567
|
+
process.stderr.write("Usage: open-classify try <message>\n");
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const configPath = join(cwd, "open-classify.config.json");
|
|
572
|
+
if (!existsSync(configPath)) {
|
|
573
|
+
process.stderr.write("✖ No open-classify.config.json — run: npx open-classify init\n");
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Try loading from the consumer's node_modules first, then fall back to the
|
|
578
|
+
// package root (useful when running from the development checkout).
|
|
579
|
+
let createClassifier;
|
|
580
|
+
const candidates = [
|
|
581
|
+
join(cwd, "node_modules", "open-classify", "dist", "src", "index.js"),
|
|
582
|
+
join(PACKAGE_ROOT, "dist", "src", "index.js"),
|
|
583
|
+
];
|
|
584
|
+
for (const candidate of candidates) {
|
|
585
|
+
if (!existsSync(candidate)) continue;
|
|
586
|
+
try {
|
|
587
|
+
const mod = await import(candidate);
|
|
588
|
+
createClassifier = mod.createClassifier;
|
|
589
|
+
break;
|
|
590
|
+
} catch { /* try next */ }
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (!createClassifier) {
|
|
594
|
+
process.stderr.write(
|
|
595
|
+
"✖ Could not load the open-classify runtime.\n" +
|
|
596
|
+
" Is the package installed? Run: npm install open-classify\n",
|
|
597
|
+
);
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const classifiersDir = join(cwd, "classifiers");
|
|
602
|
+
let classifier;
|
|
603
|
+
try {
|
|
604
|
+
classifier = createClassifier({
|
|
605
|
+
configPath,
|
|
606
|
+
extraClassifierDirs: existsSync(classifiersDir) ? [classifiersDir] : [],
|
|
607
|
+
skipResourceCheck: false,
|
|
608
|
+
});
|
|
609
|
+
} catch (err) {
|
|
610
|
+
process.stderr.write(`✖ Failed to initialise classifier: ${err.message}\n`);
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
const result = await classifier.classify({
|
|
616
|
+
messages: [{ role: "user", text: message }],
|
|
617
|
+
});
|
|
618
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
619
|
+
} catch (err) {
|
|
620
|
+
process.stderr.write(`✖ Classification failed: ${err.message}\n`);
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
// Prompt helpers
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
|
|
629
|
+
function confirm(prompt, defaultYes = false) {
|
|
630
|
+
return new Promise((resolve) => {
|
|
631
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
632
|
+
rl.question(prompt, (answer) => {
|
|
633
|
+
rl.close();
|
|
634
|
+
const v = (answer || "").trim().toLowerCase();
|
|
635
|
+
resolve(defaultYes ? (v === "" || v === "y" || v === "yes") : (v === "y" || v === "yes"));
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function promptConflict() {
|
|
641
|
+
return new Promise((resolve) => {
|
|
642
|
+
process.stdout.write("\n? Overwrite them?\n y overwrite all\n N keep existing (default)\n diff show what would change\n\n");
|
|
643
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
644
|
+
rl.question(" Choice (y/N/diff): ", (answer) => {
|
|
645
|
+
rl.close();
|
|
646
|
+
const v = (answer || "").trim().toLowerCase();
|
|
647
|
+
if (v === "y" || v === "yes") resolve("y");
|
|
648
|
+
else if (v === "diff") resolve("diff");
|
|
649
|
+
else resolve("N");
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
main().catch((err) => {
|
|
655
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
656
|
+
process.exit(1);
|
|
657
|
+
});
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import type { ClassifierInput } from "./types.js";
|
|
2
2
|
import type { ClassifierName, ClassifierRegistry, RunClassifier } from "./manifest.js";
|
|
3
3
|
import type { ClassifierOutput, RuntimeClassifierManifest } from "./stock.js";
|
|
4
|
+
export declare const BUILTIN_CLASSIFIERS_DIR: string;
|
|
4
5
|
export declare class ClassifierManifestError extends Error {
|
|
5
6
|
constructor(message: string);
|
|
6
7
|
}
|
|
8
|
+
export type ClassifierModuleMap = Readonly<Record<string, RuntimeClassifierManifest>>;
|
|
9
|
+
export interface ClassifierRegistryBundle {
|
|
10
|
+
readonly registry: ClassifierRegistry;
|
|
11
|
+
readonly modulesByName: ClassifierModuleMap;
|
|
12
|
+
readonly names: ReadonlyArray<string>;
|
|
13
|
+
}
|
|
14
|
+
export interface BuildRegistryOptions {
|
|
15
|
+
readonly extraDirs?: ReadonlyArray<string>;
|
|
16
|
+
}
|
|
7
17
|
export declare function loadClassifierRegistry(classifiersDir?: string): RuntimeClassifierManifest[];
|
|
8
|
-
export declare
|
|
9
|
-
export declare
|
|
10
|
-
export declare const MODULES_BY_NAME: Record<string, RuntimeClassifierManifest>;
|
|
18
|
+
export declare function buildClassifierRegistry(options?: BuildRegistryOptions): ClassifierRegistryBundle;
|
|
19
|
+
export declare function validateClassifierOutput(manifest: RuntimeClassifierManifest, value: unknown, model: string): ClassifierOutput;
|
|
11
20
|
export type { ClassifierName, RunClassifier };
|
|
12
|
-
export type RegistryType = typeof REGISTRY;
|
|
13
|
-
export declare function validateClassifierOutput(name: string, value: unknown, model: string): ClassifierOutput;
|
|
14
21
|
export type { ClassifierInput };
|