open-classify 0.9.1 → 0.9.3

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 CHANGED
@@ -43,23 +43,21 @@ Every classifier uses the same manifest shape and emits the same output envelope
43
43
 
44
44
  ## Getting started
45
45
 
46
- Node 18+. The packaged runner uses local Ollama with `gemma4:e4b-it-q4_K_M` as the zero-config classifier model. Pluggable via `open-classify.config.json` or a custom `RunClassifier`.
47
-
48
- **1. Install**
46
+ Prerequisites: Node 18+, [Ollama](https://ollama.com), and the default classifier model:
49
47
 
50
48
  ```sh
51
- npm install open-classify
49
+ ollama pull gemma4:e4b-it-q4_K_M
52
50
  ```
53
51
 
54
- **2. Scaffold**
52
+ **1. Scaffold (from your project root)**
55
53
 
56
54
  ```sh
57
55
  npx open-classify init
58
56
  ```
59
57
 
60
- This creates `open-classify.config.json` and a `classifiers/` directory in your project root. You'll see exactly what will be written and asked to confirm. Re-run safe: existing files are skipped.
58
+ If the package isn't installed yet, `init` offers to add it. It writes `open-classify.config.json`, `downstream-models.json`, and a `classifiers/` directory. Re-run safe: existing files are skipped. Verify the install at any time with `npx open-classify doctor`.
61
59
 
62
- **3. Use it**
60
+ **2. Use it**
63
61
 
64
62
  ```ts
65
63
  import { createClassifier } from "open-classify";
@@ -75,31 +73,29 @@ else if (result.action === "block") handleBlock(result.block_reason); // inj
75
73
  else callDownstream(result.model_id, result.tools, result.reply?.text); // route the real request
76
74
  ```
77
75
 
78
- **4. Enable or customize optional classifiers**
76
+ `createClassifier()` looks for `open-classify.config.json` in the working directory, so the scaffolded layout works with no further wiring.
77
+
78
+ **3. Enable or customize optional classifiers**
79
+
80
+ Four mandatory base classifiers (`preflight`, `model_tier`, `model_specialization`, `prompt_injection`) always run from the package. Four more (`tools`, `memory_retrieval_queries`, `conversation_digest`, `context_shift`) are optional and default to off.
79
81
 
80
- `init` writes `open-classify.config.json`, which enables package-owned stock classifiers without copying them into your project. They default to `false` so the four mandatory base classifiers run automatically:
82
+ You have two ways to use the optional ones:
81
83
 
82
84
  ```json
83
- {
84
- "classifiers": {
85
- "stock": {
86
- "tools": false
87
- }
88
- }
89
- }
85
+ { "classifiers": { "stock": { "tools": true } } }
90
86
  ```
91
87
 
92
- Set `"tools": true` to run the package-owned stock `tools` classifier. Because it stays in `node_modules/open-classify`, `npm update open-classify` can improve its prompt later without touching your project files.
88
+ Set the toggle to `true` to run the package-owned version. `npm update open-classify` keeps the prompt current.
93
89
 
94
- Inside `classifiers/` you'll also find four `_<name>/` directories editable copies of the stock classifiers, inactive because of the underscore prefix. To customize one, keep the matching `classifiers.stock.<name>` value `false`, edit the copy, then drop the underscore:
90
+ Or customize a local copy. `init` scaffolds editable templates in `classifiers/_<name>/` (inactive because of the underscore prefix). To take one over, keep the stock toggle off and rename the folder:
95
91
 
96
92
  ```sh
97
93
  mv classifiers/_tools classifiers/tools
98
94
  ```
99
95
 
100
- Edit `manifest.json` first if you need to (e.g. trim `allowed_tools` for your app). The same underscore convention works the other way too: rename `my_classifier/` → `_my_classifier/` to take any copied/custom classifier out of the active set without deleting it.
96
+ The same convention works in reverse: rename any active classifier `<name>/` `_<name>/` to deactivate without deleting.
101
97
 
102
- To write a new classifier from scratch, drop a `<name>/manifest.json` + `<name>/prompt.md` in `classifiers/`. See [docs/adding-a-classifier.md](docs/adding-a-classifier.md).
98
+ To write a new classifier, drop a `<name>/manifest.json` + `<name>/prompt.md` in `classifiers/`. See [docs/adding-a-classifier.md](docs/adding-a-classifier.md).
103
99
 
104
100
  ### Classifying assistant output
105
101
 
@@ -310,45 +306,20 @@ The resolver picks the cheapest model matching `model_specialization` and `model
310
306
  - Open Classify keeps whole messages only, drops oldest first to fit a 5,000-char budget, and caps history at 20 messages.
311
307
  - Unknown fields are rejected, not passed through.
312
308
 
313
- ## Local setup
309
+ ## Configuration
314
310
 
315
- ```sh
316
- npm run setup
317
- ```
318
-
319
- Checks prerequisites (Node, npm, Ollama), confirms the base model is pulled, installs dependencies, and builds. Idempotent — safe to re-run.
320
-
321
- Optional Ollama runtime config:
311
+ `npx open-classify init` writes a working `open-classify.config.json` for you. To customize, edit it directly — the full set of supported fields (with realistic example values) lives in [open-classify.config.example.json](open-classify.config.example.json).
322
312
 
323
- ```sh
324
- cp open-classify.config.example.json open-classify.config.json
325
- ```
326
-
327
- ```json
328
- {
329
- "runner": {
330
- "provider": "ollama",
331
- "defaultModel": "gemma4:e4b-it-q4_K_M",
332
- "models": {
333
- "model_tier": "qwen2.5:7b-instruct-q4_K_M",
334
- "prompt_injection": "llama-guard3:8b",
335
- "memory_retrieval_queries": "qwen2.5:7b-instruct-q4_K_M"
336
- }
337
- },
338
- "catalog": "downstream-models.json",
339
- "classifiers": {
340
- "dirs": ["classifiers"],
341
- "stock": {
342
- "tools": false,
343
- "memory_retrieval_queries": false,
344
- "conversation_digest": false,
345
- "context_shift": false
346
- }
347
- }
348
- }
349
- ```
350
-
351
- `runner.provider` currently supports `"ollama"` only. `runner.defaultModel` applies to any classifier without an explicit `runner.models` entry. `runner.models` is a flat map keyed by classifier name. `classifiers.dirs` is for user-owned copied/custom classifiers; `classifiers.stock` toggles package-owned optional classifiers.
313
+ | Field | What it controls |
314
+ |---|---|
315
+ | `runner.provider` | Backend. Currently `"ollama"` only. |
316
+ | `runner.host` | Ollama host URL. Defaults to `http://127.0.0.1:11434`. |
317
+ | `runner.defaultModel` | Classifier model used when there is no per-classifier override. |
318
+ | `runner.options` | Ollama generation options: `temperature`, `top_p`, `seed`, `num_ctx`. |
319
+ | `runner.models` | Per-classifier model overrides. Flat map keyed by classifier name. |
320
+ | `catalog` | Path to the downstream model catalog (relative to the config file). |
321
+ | `classifiers.dirs` | Directories of user-owned classifiers to load. |
322
+ | `classifiers.stock` | Toggles for package-owned optional stock classifiers. |
352
323
 
353
324
  ## Bring your own backend
354
325
 
@@ -383,8 +354,6 @@ For the lowest-level entry points, `classifyOpenClassifyInput(input, { runClassi
383
354
  - [docs/resolver.md](docs/resolver.md) — aggregation and model resolution
384
355
  - [docs/adding-a-classifier.md](docs/adding-a-classifier.md) — author guide
385
356
 
386
- ## Development
357
+ ## Contributing
387
358
 
388
- ```sh
389
- npm test # build + run the Node test runner suite
390
- ```
359
+ Clone the repo, then `npm run setup` (checks Node/Ollama, pulls the base model, installs and builds) and `npm test` (build + Node test runner). PRs welcome.
@@ -35,55 +35,29 @@ const TEMPLATE_DESCRIPTIONS = {
35
35
 
36
36
  const CLASSIFIERS_README = `# classifiers/
37
37
 
38
- Drop a folder here per classifier. Each folder needs:
38
+ Each classifier is a folder with two files:
39
39
 
40
- - \`manifest.json\` — see [open-classify docs](https://github.com/taylorbayouth/open-classify/blob/main/docs/adding-a-classifier.md)
41
- - \`prompt.md\` — the classifier-specific instructions
40
+ - \`manifest.json\` — declares the output shape and fallback
41
+ - \`prompt.md\` — the classification instructions
42
42
 
43
- ## Quickstart
43
+ The loader skips any folder whose name starts with \`_\`. That's how the
44
+ four \`_<name>/\` templates here stay inactive until you opt in: drop the
45
+ underscore (\`mv _tools tools\`) and the classifier runs on the next start.
44
46
 
45
- \`\`\`js
46
- import { createClassifier } from "open-classify";
47
+ Each template mirrors a package-owned stock classifier. You have two ways
48
+ to use them:
47
49
 
48
- const { classify } = createClassifier();
49
- \`\`\`
50
+ 1. **Enable in place** set \`classifiers.stock.<name>: true\` in
51
+ \`open-classify.config.json\`. The package-owned version runs and is
52
+ updated by \`npm update open-classify\`.
53
+ 2. **Customize a local copy** — keep the stock toggle off, drop the
54
+ underscore on the template here, and edit \`prompt.md\` /
55
+ \`manifest.json\` to taste.
50
56
 
51
- Place this in your server entry point. Call \`classify(input)\` for each user message.
52
- \`open-classify.config.json\` wires in \`./classifiers\` automatically.
53
-
54
- ## Stock classifiers
55
-
56
- \`tools\`, \`memory_retrieval_queries\`, \`conversation_digest\`, and \`context_shift\`
57
- ship with the package but are disabled by default in \`open-classify.config.json\`.
58
- Enable a package-owned stock classifier by setting it to \`true\`:
59
-
60
- \`\`\`json
61
- {
62
- "classifiers": {
63
- "stock": {
64
- "tools": true
65
- }
66
- }
67
- }
68
- \`\`\`
69
-
70
- Package-owned stock classifiers are updated by \`npm update open-classify\`.
71
-
72
- ## Customizing a stock classifier
73
-
74
- The four \`_<name>/\` directories below are editable copies of the stock classifiers.
75
- They are inactive because the loader skips any folder starting with \`_\`. To customize one,
76
- keep the matching \`classifiers.stock.<name>\` value \`false\`, edit the files, then drop the underscore:
77
-
78
- \`\`\`sh
79
- mv _tools tools
80
- \`\`\`
81
-
82
- You probably also want to edit its \`manifest.json\` first to fit your app (e.g. trim the \`allowed_tools\` list).
83
-
84
- ## Deactivating without deleting
85
-
86
- Same trick in reverse — rename \`my_classifier\` → \`_my_classifier\` to take it out of the active set without losing your work.
57
+ To write your own classifier, drop a new \`<name>/\` folder here with its
58
+ own \`manifest.json\` and \`prompt.md\`. The folder name must match the
59
+ manifest's \`name\` field. See the
60
+ [author guide](https://github.com/taylorbayouth/open-classify/blob/main/docs/adding-a-classifier.md).
87
61
  `;
88
62
 
89
63
  const DEFAULT_CONFIG = {
@@ -186,6 +160,8 @@ function parseUninstallFlags(args) {
186
160
  yes: false,
187
161
  dryRun: false,
188
162
  force: false,
163
+ keepPackage: false,
164
+ packageManager: null,
189
165
  classifierDir: "classifiers",
190
166
  };
191
167
 
@@ -194,6 +170,9 @@ function parseUninstallFlags(args) {
194
170
  if (arg === "--yes" || arg === "-y") flags.yes = true;
195
171
  else if (arg === "--dry-run") flags.dryRun = true;
196
172
  else if (arg === "--force") flags.force = true;
173
+ else if (arg === "--keep-package") flags.keepPackage = true;
174
+ else if (arg === "--package-manager" && args[i + 1]) flags.packageManager = args[++i];
175
+ else if (arg.startsWith("--package-manager=")) flags.packageManager = arg.split("=")[1];
197
176
  else if (arg === "--classifier-dir" && args[i + 1]) flags.classifierDir = args[++i];
198
177
  else if (arg.startsWith("--classifier-dir=")) flags.classifierDir = arg.split("=")[1];
199
178
  }
@@ -208,9 +187,10 @@ Commands:
208
187
  init [options] Scaffold open-classify.config.json and classifiers/ in the
209
188
  current directory. Re-run safe: existing files are skipped.
210
189
 
211
- uninstall Remove open-classify scaffold files from the current
212
- directory. Use --force to remove the whole classifiers/
213
- directory, including active/custom classifiers.
190
+ uninstall Remove the open-classify scaffold and uninstall the
191
+ package. Use --force to also delete active/custom
192
+ classifiers, --keep-package to leave the npm dependency
193
+ in place.
214
194
 
215
195
  doctor Check that the install, config, Ollama, and classifiers are
216
196
  all working. Exits non-zero on failure.
@@ -231,6 +211,8 @@ Options for init:
231
211
  Options for uninstall:
232
212
  --dry-run Preview what would be removed; don't delete anything
233
213
  --force Remove the whole classifiers/ directory
214
+ --keep-package Don't run the package manager uninstall step
215
+ --package-manager <m> npm | pnpm | yarn | bun (default: auto-detect)
234
216
  --classifier-dir <p> Directory for classifiers (default: ./classifiers)
235
217
  --yes, -y Accept all prompts (CI mode)
236
218
 
@@ -389,21 +371,31 @@ async function runInit({ cwd, yes, minimal, dryRun, force, noInstall, packageMan
389
371
  }
390
372
  }
391
373
 
392
- const classifierDirRel = relative(cwd, resolvedClassifierDir);
393
374
  process.stdout.write(`
394
375
  Next steps:
395
376
 
396
- 1. Pull the default model:
377
+ 1. Pull the default classifier model:
378
+
397
379
  ollama pull ${config.runner.defaultModel}
398
380
 
399
- 2. Wire it into your server (example for a Node entrypoint):
400
- see ./${classifierDirRel}/README.md → "Quickstart"
381
+ 2. Verify everything is wired up:
401
382
 
402
- 3. Verify the install:
403
383
  npx open-classify doctor
404
384
 
405
- 4. Run a one-shot classification against your config:
406
- npx open-classify try "hello world"
385
+ 3. Try it without writing any code:
386
+
387
+ npx open-classify try "hello"
388
+
389
+ 4. Use it from your code:
390
+
391
+ import { createClassifier } from "open-classify";
392
+ const { classify } = createClassifier();
393
+ const result = await classify({
394
+ messages: [{ role: "user", text: "hello" }],
395
+ });
396
+
397
+ The factory finds open-classify.config.json in your working
398
+ directory and wires in the classifiers/ folder automatically.
407
399
 
408
400
  Docs: https://github.com/taylorbayouth/open-classify#readme
409
401
  `);
@@ -562,12 +554,21 @@ function showDiffs(conflicts, cwd, classifierDir, config = DEFAULT_CONFIG) {
562
554
  // uninstall
563
555
  // ---------------------------------------------------------------------------
564
556
 
565
- async function runUninstall({ cwd, yes, dryRun, force, classifierDir }) {
557
+ async function runUninstall({ cwd, yes, dryRun, force, keepPackage, packageManager, classifierDir }) {
566
558
  const resolvedClassifierDir = resolve(cwd, classifierDir);
567
559
  const plan = planUninstall(cwd, { classifierDir: resolvedClassifierDir, force });
568
560
 
569
- if (plan.toRemove.length === 0) {
570
- process.stdout.write("Nothing to remove — no open-classify scaffold found.\n");
561
+ const pkgPath = join(cwd, "package.json");
562
+ let pkg = null;
563
+ if (existsSync(pkgPath)) {
564
+ try { pkg = JSON.parse(readFileSync(pkgPath, "utf8")); } catch { pkg = null; }
565
+ }
566
+ const packageInstalled = pkg !== null && isOpenClassifyDep(pkg);
567
+ const willRemovePackage = !keepPackage && packageInstalled;
568
+ const pm = packageManager || detectPackageManager(cwd);
569
+
570
+ if (plan.toRemove.length === 0 && !willRemovePackage) {
571
+ process.stdout.write("Nothing to remove — no open-classify scaffold or dependency found.\n");
571
572
  if (plan.toSkip.length > 0) {
572
573
  process.stdout.write("\nSkipped active/custom classifier dirs:\n");
573
574
  for (const p of plan.toSkip) process.stdout.write(` ${p}\n`);
@@ -576,8 +577,11 @@ async function runUninstall({ cwd, yes, dryRun, force, classifierDir }) {
576
577
  return;
577
578
  }
578
579
 
579
- process.stdout.write(`\nThe following open-classify scaffold will be removed from ${cwd}:\n\n`);
580
+ process.stdout.write(`\nThe following will be removed from ${cwd}:\n\n`);
580
581
  for (const p of plan.toRemove) process.stdout.write(` ${p}\n`);
582
+ if (willRemovePackage) {
583
+ process.stdout.write(` open-classify (via ${pm} uninstall)\n`);
584
+ }
581
585
 
582
586
  if (plan.toSkip.length > 0) {
583
587
  process.stdout.write("\nSkipped active/custom classifier dirs:\n");
@@ -585,6 +589,10 @@ async function runUninstall({ cwd, yes, dryRun, force, classifierDir }) {
585
589
  process.stdout.write("\nUse --force to remove the whole classifiers/ directory.\n");
586
590
  }
587
591
 
592
+ if (keepPackage && packageInstalled) {
593
+ process.stdout.write("\nKeeping the open-classify package (--keep-package).\n");
594
+ }
595
+
588
596
  if (dryRun) {
589
597
  process.stdout.write("\n(dry run — nothing removed)\n");
590
598
  return;
@@ -600,8 +608,19 @@ async function runUninstall({ cwd, yes, dryRun, force, classifierDir }) {
600
608
 
601
609
  process.stdout.write("\n");
602
610
  for (const action of plan.actions) action();
603
- process.stdout.write("\n✓ removed open-classify scaffold\n");
604
- process.stdout.write("To remove the package dependency too, run: npm uninstall open-classify\n");
611
+ if (plan.toRemove.length > 0) {
612
+ process.stdout.write("\n✓ removed open-classify scaffold\n");
613
+ }
614
+
615
+ if (willRemovePackage) {
616
+ process.stdout.write(`\n Running: ${pm} uninstall open-classify\n\n`);
617
+ const result = spawnSync(pm, ["uninstall", "open-classify"], { cwd, stdio: "inherit" });
618
+ if (result.status !== 0) {
619
+ process.stderr.write(`\n✖ Package uninstall failed. Run manually: ${pm} uninstall open-classify\n`);
620
+ process.exit(1);
621
+ }
622
+ process.stdout.write("\n✓ removed open-classify package\n");
623
+ }
605
624
  }
606
625
 
607
626
  function planUninstall(cwd, { classifierDir, force }) {
@@ -1,13 +1,6 @@
1
1
  # Adding a classifier
2
2
 
3
- Every classifier — package-owned or your own — uses the same two-file layout. The runtime only cares about which reserved fields a classifier opts into; ownership just decides whether `npm update open-classify` can replace the prompt.
4
-
5
- There are two places a classifier can live:
6
-
7
- - **In your own app**, in a directory listed in `open-classify.config.json` under `classifiers.dirs` (almost always `./classifiers/` after `npx open-classify init`). This is the right path when you've installed Open Classify as a dependency.
8
- - **In this repo**, under `src/classifiers/<name>/`. Only do this when you're contributing a new mandatory built-in back to Open Classify.
9
-
10
- Either way, the layout and contract are identical.
3
+ Every classifier uses the same two-file layout. Drop a folder into a directory listed under `classifiers.dirs` in `open-classify.config.json` (defaults to `./classifiers/` after `npx open-classify init`) and the runtime picks it up on the next start.
11
4
 
12
5
  ## 1. Create the directory
13
6
 
package/docs/manifests.md CHANGED
@@ -1,16 +1,15 @@
1
1
  # Manifest reference
2
2
 
3
- Every classifier lives in `src/classifiers/<name>/` and contains exactly two files:
3
+ Every classifier is a directory with exactly two files:
4
4
 
5
5
  ```
6
- src/classifiers/
7
- _prompts/ # shared base markdown (base.md, reason.md, confidence.md)
6
+ classifiers/
8
7
  <classifier_name>/
9
8
  manifest.json
10
9
  prompt.md
11
10
  ```
12
11
 
13
- The loader skips any top-level directory whose name starts with `_` (those are shared assets, not classifiers).
12
+ Folders whose names start with `_` are skipped by the loader — that's how the scaffolded `_<name>/` templates stay inactive until you drop the underscore.
14
13
 
15
14
  ## Fields
16
15
 
@@ -145,7 +144,7 @@ A manifest may declare both reserved fields and custom properties; they sit alon
145
144
 
146
145
  `prompt.md` is the classifier-specific instruction text. The runtime composes the system prompt at load time from:
147
146
 
148
- 1. Shared base sections (JSON-only contract, `reason` + `certainty` rules) from `src/classifiers/_prompts/`
147
+ 1. Shared base sections (JSON-only contract, `reason` + `certainty` rules)
149
148
  2. The classifier header (name and purpose, with the purpose stated as a hard scope boundary)
150
149
  3. Auto-injected fragments for each declared reserved field (canonical enum values included, so you can't drift)
151
150
  4. Your `prompt.md`
@@ -4,16 +4,13 @@
4
4
  "host": "http://127.0.0.1:11434",
5
5
  "defaultModel": "gemma4:e4b-it-q4_K_M",
6
6
  "options": {
7
- "num_ctx": 4096,
8
7
  "temperature": 0,
9
8
  "top_p": 1,
10
- "seed": 0
9
+ "seed": 0,
10
+ "num_ctx": 4096
11
11
  },
12
12
  "models": {
13
- "preflight": "gemma4:e4b-it-q4_K_M",
14
- "model_tier": "gemma4:e4b-it-q4_K_M",
15
- "model_specialization": "gemma4:e4b-it-q4_K_M",
16
- "prompt_injection": "gemma4:e4b-it-q4_K_M"
13
+ "prompt_injection": "llama-guard3:8b"
17
14
  }
18
15
  },
19
16
  "catalog": "downstream-models.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-classify",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Manifest-driven classifier runtime for routing user messages to downstream AI models",
5
5
  "license": "MIT",
6
6
  "author": "Taylor Bayouth",