open-classify 0.9.2 → 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/CHANGELOG.md +89 -0
- package/README.md +86 -137
- package/bin/open-classify.mjs +268 -650
- package/dist/src/classifiers.js +25 -18
- package/dist/src/config.d.ts +2 -2
- package/dist/src/config.js +18 -16
- package/docs/adding-a-classifier.md +15 -14
- package/docs/manifests.md +1 -1
- package/package.json +2 -3
- package/templates/scaffold/open-classify/README.md +46 -0
- package/templates/scaffold/open-classify/classifiers/README.md +22 -0
- package/templates/scaffold/open-classify/config.json +12 -0
- package/open-classify.config.example.json +0 -26
- /package/{downstream-models.json → templates/scaffold/open-classify/downstream-models.json} +0 -0
- /package/templates/{context_shift → stock/context_shift}/manifest.json +0 -0
- /package/templates/{context_shift → stock/context_shift}/prompt.md +0 -0
- /package/templates/{conversation_digest → stock/conversation_digest}/manifest.json +0 -0
- /package/templates/{conversation_digest → stock/conversation_digest}/prompt.md +0 -0
- /package/templates/{memory_retrieval_queries → stock/memory_retrieval_queries}/manifest.json +0 -0
- /package/templates/{memory_retrieval_queries → stock/memory_retrieval_queries}/prompt.md +0 -0
- /package/templates/{tools → stock/tools}/manifest.json +0 -0
- /package/templates/{tools → stock/tools}/prompt.md +0 -0
package/bin/open-classify.mjs
CHANGED
|
@@ -1,177 +1,69 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// open-classify CLI.
|
|
2
|
+
// open-classify CLI.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// init Copy the scaffold (open-classify/) into the current directory.
|
|
5
|
+
// eject <name> Copy a stock classifier into open-classify/classifiers/<name>/.
|
|
6
|
+
// doctor Verify install, config, Ollama, and classifiers.
|
|
7
|
+
// try <message> Run the pipeline against a single message.
|
|
8
|
+
//
|
|
9
|
+
// Removal is intentionally not a subcommand — `rm -rf open-classify/` and
|
|
10
|
+
// `npm uninstall open-classify` cover it, and bundling them creates more
|
|
11
|
+
// confusion than convenience (notably the npx "needs to install" prompt
|
|
12
|
+
// when the package isn't a dep yet).
|
|
8
13
|
|
|
9
|
-
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync,
|
|
14
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
15
|
import { createInterface } from "node:readline";
|
|
11
|
-
import {
|
|
16
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
12
17
|
import { fileURLToPath } from "node:url";
|
|
13
|
-
import { spawnSync } from "node:child_process";
|
|
14
18
|
|
|
15
19
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
16
20
|
const PACKAGE_ROOT = resolve(SCRIPT_DIR, "..");
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const TEMPLATE_NAMES = ["conversation_digest", "context_shift", "memory_retrieval_queries", "tools"];
|
|
22
|
-
const STOCK_CONFIG = {
|
|
23
|
-
tools: false,
|
|
24
|
-
memory_retrieval_queries: false,
|
|
25
|
-
conversation_digest: false,
|
|
26
|
-
context_shift: false,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const TEMPLATE_DESCRIPTIONS = {
|
|
30
|
-
conversation_digest: "rolling summary of recent turns",
|
|
31
|
-
context_shift: "detects topic changes",
|
|
32
|
-
memory_retrieval_queries: "generates queries for a memory store",
|
|
33
|
-
tools: "tool-call routing",
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const CLASSIFIERS_README = `# classifiers/
|
|
37
|
-
|
|
38
|
-
Each classifier is a folder with two files:
|
|
39
|
-
|
|
40
|
-
- \`manifest.json\` — declares the output shape and fallback
|
|
41
|
-
- \`prompt.md\` — the classification instructions
|
|
42
|
-
|
|
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.
|
|
46
|
-
|
|
47
|
-
Each template mirrors a package-owned stock classifier. You have two ways
|
|
48
|
-
to use them:
|
|
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.
|
|
56
|
-
|
|
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).
|
|
61
|
-
`;
|
|
62
|
-
|
|
63
|
-
const DEFAULT_CONFIG = {
|
|
64
|
-
runner: {
|
|
65
|
-
provider: "ollama",
|
|
66
|
-
host: "http://127.0.0.1:11434",
|
|
67
|
-
defaultModel: "gemma4:e4b-it-q4_K_M",
|
|
68
|
-
},
|
|
69
|
-
catalog: DOWNSTREAM_MODELS_FILENAME,
|
|
70
|
-
classifiers: {
|
|
71
|
-
dirs: ["classifiers"],
|
|
72
|
-
stock: STOCK_CONFIG,
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
function configForInit({ minimal }) {
|
|
77
|
-
if (!minimal) return DEFAULT_CONFIG;
|
|
78
|
-
return {
|
|
79
|
-
...DEFAULT_CONFIG,
|
|
80
|
-
classifiers: {
|
|
81
|
-
stock: STOCK_CONFIG,
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
21
|
+
const SCAFFOLD_DIR = join(PACKAGE_ROOT, "templates", "scaffold", "open-classify");
|
|
22
|
+
const STOCK_DIR = join(PACKAGE_ROOT, "templates", "stock");
|
|
23
|
+
const PROJECT_DIRNAME = "open-classify";
|
|
24
|
+
const STOCK_NAMES = ["tools", "memory_retrieval_queries", "conversation_digest", "context_shift"];
|
|
85
25
|
|
|
86
26
|
// ---------------------------------------------------------------------------
|
|
87
27
|
// Entry point
|
|
88
28
|
// ---------------------------------------------------------------------------
|
|
89
29
|
|
|
90
30
|
async function main() {
|
|
91
|
-
const
|
|
92
|
-
const subcommand = args[0];
|
|
31
|
+
const [subcommand, ...rest] = process.argv.slice(2);
|
|
93
32
|
|
|
94
33
|
if (!subcommand || subcommand === "-h" || subcommand === "--help") {
|
|
95
34
|
printHelp();
|
|
96
35
|
process.exit(subcommand ? 0 : 1);
|
|
97
36
|
}
|
|
98
37
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
await runTry({ cwd: process.cwd(), message });
|
|
119
|
-
return;
|
|
38
|
+
switch (subcommand) {
|
|
39
|
+
case "init":
|
|
40
|
+
await runInit({ cwd: process.cwd(), ...parseFlags(rest) });
|
|
41
|
+
return;
|
|
42
|
+
case "eject":
|
|
43
|
+
await runEject({ cwd: process.cwd(), name: rest[0], ...parseFlags(rest.slice(1)) });
|
|
44
|
+
return;
|
|
45
|
+
case "doctor":
|
|
46
|
+
await runDoctor({ cwd: process.cwd() });
|
|
47
|
+
return;
|
|
48
|
+
case "try": {
|
|
49
|
+
const message = rest.join(" ");
|
|
50
|
+
await runTry({ cwd: process.cwd(), message });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
default:
|
|
54
|
+
process.stderr.write(`Unknown subcommand: ${subcommand}\n\n`);
|
|
55
|
+
printHelp();
|
|
56
|
+
process.exit(1);
|
|
120
57
|
}
|
|
121
|
-
|
|
122
|
-
console.error(`Unknown subcommand: ${subcommand}`);
|
|
123
|
-
printHelp();
|
|
124
|
-
process.exit(1);
|
|
125
58
|
}
|
|
126
59
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
function parseInitFlags(args) {
|
|
132
|
-
const flags = {
|
|
133
|
-
yes: false,
|
|
134
|
-
minimal: false,
|
|
135
|
-
dryRun: false,
|
|
136
|
-
force: false,
|
|
137
|
-
noInstall: false,
|
|
138
|
-
packageManager: null,
|
|
139
|
-
classifierDir: "classifiers",
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
for (let i = 0; i < args.length; i++) {
|
|
143
|
-
const arg = args[i];
|
|
60
|
+
function parseFlags(args) {
|
|
61
|
+
const flags = { yes: false, force: false, dryRun: false };
|
|
62
|
+
for (const arg of args) {
|
|
144
63
|
if (arg === "--yes" || arg === "-y") flags.yes = true;
|
|
145
|
-
else if (arg === "--minimal") flags.minimal = true;
|
|
146
|
-
else if (arg === "--dry-run") flags.dryRun = true;
|
|
147
64
|
else if (arg === "--force") flags.force = true;
|
|
148
|
-
else if (arg === "--no-install") flags.noInstall = true;
|
|
149
|
-
else if (arg === "--package-manager" && args[i + 1]) flags.packageManager = args[++i];
|
|
150
|
-
else if (arg.startsWith("--package-manager=")) flags.packageManager = arg.split("=")[1];
|
|
151
|
-
else if (arg === "--classifier-dir" && args[i + 1]) flags.classifierDir = args[++i];
|
|
152
|
-
else if (arg.startsWith("--classifier-dir=")) flags.classifierDir = arg.split("=")[1];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return flags;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function parseUninstallFlags(args) {
|
|
159
|
-
const flags = {
|
|
160
|
-
yes: false,
|
|
161
|
-
dryRun: false,
|
|
162
|
-
force: false,
|
|
163
|
-
classifierDir: "classifiers",
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
for (let i = 0; i < args.length; i++) {
|
|
167
|
-
const arg = args[i];
|
|
168
|
-
if (arg === "--yes" || arg === "-y") flags.yes = true;
|
|
169
65
|
else if (arg === "--dry-run") flags.dryRun = true;
|
|
170
|
-
else if (arg === "--force") flags.force = true;
|
|
171
|
-
else if (arg === "--classifier-dir" && args[i + 1]) flags.classifierDir = args[++i];
|
|
172
|
-
else if (arg.startsWith("--classifier-dir=")) flags.classifierDir = arg.split("=")[1];
|
|
173
66
|
}
|
|
174
|
-
|
|
175
67
|
return flags;
|
|
176
68
|
}
|
|
177
69
|
|
|
@@ -179,161 +71,69 @@ function printHelp() {
|
|
|
179
71
|
process.stdout.write(`open-classify — runtime CLI
|
|
180
72
|
|
|
181
73
|
Commands:
|
|
182
|
-
init
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
uninstall Remove open-classify scaffold files from the current
|
|
186
|
-
directory. Use --force to remove the whole classifiers/
|
|
187
|
-
directory, including active/custom classifiers.
|
|
188
|
-
|
|
189
|
-
doctor Check that the install, config, Ollama, and classifiers are
|
|
190
|
-
all working. Exits non-zero on failure.
|
|
191
|
-
|
|
192
|
-
try <message> Run the pipeline against a single message and print the
|
|
193
|
-
result. Useful for verifying your setup without touching
|
|
194
|
-
application code.
|
|
195
|
-
|
|
196
|
-
Options for init:
|
|
197
|
-
--minimal Write runtime config/catalog only; skip classifiers/
|
|
198
|
-
--dry-run Preview what would be created; don't write anything
|
|
199
|
-
--force Overwrite existing files without prompting
|
|
200
|
-
--no-install Skip the "add to package.json" prompt
|
|
201
|
-
--package-manager <m> npm | pnpm | yarn | bun (default: auto-detect)
|
|
202
|
-
--classifier-dir <p> Directory for classifiers (default: ./classifiers)
|
|
203
|
-
--yes, -y Accept all prompts (CI mode)
|
|
204
|
-
|
|
205
|
-
Options for uninstall:
|
|
206
|
-
--dry-run Preview what would be removed; don't delete anything
|
|
207
|
-
--force Remove the whole classifiers/ directory
|
|
208
|
-
--classifier-dir <p> Directory for classifiers (default: ./classifiers)
|
|
209
|
-
--yes, -y Accept all prompts (CI mode)
|
|
74
|
+
init Scaffold ./open-classify/ in the current directory.
|
|
75
|
+
Re-run safe: existing files are skipped unless --force.
|
|
210
76
|
|
|
211
|
-
|
|
212
|
-
|
|
77
|
+
eject <name> Copy a stock classifier into ./open-classify/classifiers/<name>/
|
|
78
|
+
so you can edit it. Stock classifiers:
|
|
79
|
+
${STOCK_NAMES.join(", ")}
|
|
213
80
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
217
|
-
if (existsSync(join(cwd, "bun.lockb"))) return "bun";
|
|
218
|
-
return "npm";
|
|
219
|
-
}
|
|
81
|
+
doctor Verify install, config, Ollama, and classifiers.
|
|
82
|
+
Exits non-zero on failure.
|
|
220
83
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
(f) => pkg[f]?.["open-classify"],
|
|
224
|
-
);
|
|
225
|
-
}
|
|
84
|
+
try <message> Run the pipeline against a single message and print
|
|
85
|
+
the result.
|
|
226
86
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
87
|
+
Options:
|
|
88
|
+
--yes, -y Accept all prompts (CI mode)
|
|
89
|
+
--force Overwrite existing files
|
|
90
|
+
--dry-run Preview what would change; don't write anything
|
|
91
|
+
|
|
92
|
+
Setup:
|
|
93
|
+
npm install open-classify
|
|
94
|
+
npx open-classify init
|
|
95
|
+
|
|
96
|
+
Removal:
|
|
97
|
+
rm -rf open-classify/
|
|
98
|
+
npm uninstall open-classify
|
|
99
|
+
|
|
100
|
+
Docs: https://github.com/taylorbayouth/open-classify#readme
|
|
101
|
+
`);
|
|
233
102
|
}
|
|
234
103
|
|
|
235
104
|
// ---------------------------------------------------------------------------
|
|
236
105
|
// init
|
|
237
106
|
// ---------------------------------------------------------------------------
|
|
238
107
|
|
|
239
|
-
async function runInit({ cwd, yes,
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
if (!existsSync(pkgPath)) {
|
|
243
|
-
process.stderr.write(
|
|
244
|
-
`✖ No package.json found in ${cwd}.\n` +
|
|
245
|
-
` open-classify scaffolds code that imports the library, so it needs a\n` +
|
|
246
|
-
` Node project to live in.\n\n` +
|
|
247
|
-
` Create one first: npm init -y\n`,
|
|
248
|
-
);
|
|
249
|
-
process.exit(1);
|
|
250
|
-
}
|
|
108
|
+
async function runInit({ cwd, yes, force, dryRun }) {
|
|
109
|
+
requireHostProject(cwd);
|
|
110
|
+
warnIfPackageMissing(cwd);
|
|
251
111
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
255
|
-
} catch {
|
|
256
|
-
process.stderr.write(`✖ Could not parse package.json at ${pkgPath}\n`);
|
|
257
|
-
process.exit(1);
|
|
258
|
-
}
|
|
112
|
+
const destRoot = join(cwd, PROJECT_DIRNAME);
|
|
113
|
+
const plan = planScaffoldCopy(SCAFFOLD_DIR, destRoot, cwd, force);
|
|
259
114
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (doInstall) {
|
|
266
|
-
const pm = packageManager || detectPackageManager(cwd);
|
|
267
|
-
const installCmd = pm === "npm" ? ["install", "open-classify"] : ["add", "open-classify"];
|
|
268
|
-
process.stdout.write(`\n Running: ${pm} ${installCmd.join(" ")}\n\n`);
|
|
269
|
-
const result = spawnSync(pm, installCmd, { cwd, stdio: "inherit" });
|
|
270
|
-
if (result.status !== 0) {
|
|
271
|
-
process.stderr.write(`\n✖ Install failed. Run manually: ${pm} ${installCmd.join(" ")}\n`);
|
|
272
|
-
process.exit(1);
|
|
273
|
-
}
|
|
274
|
-
installedNow = true;
|
|
275
|
-
process.stdout.write("\n");
|
|
276
|
-
} else {
|
|
277
|
-
process.stdout.write(
|
|
278
|
-
` Skipped. You'll need to run \`npm install open-classify\` before importing.\n\n`,
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// 3. Plan.
|
|
284
|
-
const resolvedClassifierDir = resolve(cwd, classifierDir);
|
|
285
|
-
const config = configForInit({ minimal });
|
|
286
|
-
const wrote = { config: false, catalog: false, readme: false, templateCount: 0 };
|
|
287
|
-
let plan = planInit(cwd, { minimal, classifierDir: resolvedClassifierDir, force, wrote, config });
|
|
288
|
-
|
|
289
|
-
// Nothing to do.
|
|
290
|
-
if (plan.toCreate.length === 0) {
|
|
291
|
-
process.stdout.write("Nothing to do — your project already has all the scaffolded files.\n");
|
|
292
|
-
if (plan.toSkip.length > 0) {
|
|
293
|
-
process.stdout.write("\nAlready in place:\n");
|
|
294
|
-
for (const p of plan.toSkip) process.stdout.write(` ${p}\n`);
|
|
115
|
+
if (plan.actions.length === 0) {
|
|
116
|
+
process.stdout.write(`Nothing to do — ./${PROJECT_DIRNAME}/ already has every scaffold file.\n`);
|
|
117
|
+
if (plan.skipped.length > 0) {
|
|
118
|
+
process.stdout.write("\nAlready present (use --force to overwrite):\n");
|
|
119
|
+
for (const p of plan.skipped) process.stdout.write(` ${p}\n`);
|
|
295
120
|
}
|
|
296
121
|
return;
|
|
297
122
|
}
|
|
298
123
|
|
|
299
|
-
// 4. Preview.
|
|
300
124
|
process.stdout.write(`\nThe following will be created in ${cwd}:\n\n`);
|
|
301
|
-
for (const item of plan.preview) {
|
|
302
|
-
if (item.isGroupHeader) {
|
|
303
|
-
process.stdout.write(` ${item.label}\n`);
|
|
304
|
-
} else if (item.indent) {
|
|
305
|
-
process.stdout.write(` ${item.label.padEnd(32)} ${item.description}\n`);
|
|
306
|
-
} else {
|
|
307
|
-
process.stdout.write(` ${item.label.padEnd(34)} ${item.description}\n`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
125
|
+
for (const item of plan.preview) process.stdout.write(` ${item}\n`);
|
|
310
126
|
|
|
311
|
-
if (plan.
|
|
312
|
-
process.stdout.write(`\
|
|
313
|
-
for (const p of plan.
|
|
127
|
+
if (plan.skipped.length > 0) {
|
|
128
|
+
process.stdout.write(`\nAlready present (use --force to overwrite):\n`);
|
|
129
|
+
for (const p of plan.skipped) process.stdout.write(` ${p}\n`);
|
|
314
130
|
}
|
|
315
131
|
|
|
316
|
-
// 5. Stop here on --dry-run.
|
|
317
132
|
if (dryRun) {
|
|
318
133
|
process.stdout.write("\n(dry run — nothing written)\n");
|
|
319
134
|
return;
|
|
320
135
|
}
|
|
321
136
|
|
|
322
|
-
// 6. Conflict handling: interactive only (not --yes, not --force).
|
|
323
|
-
if (plan.toSkip.length > 0 && !yes && !force) {
|
|
324
|
-
const choice = await promptConflict();
|
|
325
|
-
if (choice === "diff") {
|
|
326
|
-
showDiffs(plan.toSkip, cwd, resolvedClassifierDir, config);
|
|
327
|
-
const choice2 = await promptConflict();
|
|
328
|
-
if (choice2 === "y") {
|
|
329
|
-
plan = planInit(cwd, { minimal, classifierDir: resolvedClassifierDir, force: true, wrote, config });
|
|
330
|
-
}
|
|
331
|
-
} else if (choice === "y") {
|
|
332
|
-
plan = planInit(cwd, { minimal, classifierDir: resolvedClassifierDir, force: true, wrote, config });
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// 7. Confirm (skip in --yes mode).
|
|
337
137
|
if (!yes) {
|
|
338
138
|
const proceed = await confirm("\n? Continue? (Y/n) ", true);
|
|
339
139
|
if (!proceed) {
|
|
@@ -342,235 +142,116 @@ async function runInit({ cwd, yes, minimal, dryRun, force, noInstall, packageMan
|
|
|
342
142
|
}
|
|
343
143
|
}
|
|
344
144
|
|
|
345
|
-
// 8. Execute.
|
|
346
145
|
process.stdout.write("\n");
|
|
347
146
|
for (const action of plan.actions) action();
|
|
348
147
|
|
|
349
|
-
|
|
350
|
-
process.stdout.write("\n");
|
|
351
|
-
if (installedNow) {
|
|
352
|
-
const v = getCliVersion();
|
|
353
|
-
process.stdout.write(`✓ open-classify installed${v ? ` (v${v})` : ""}\n`);
|
|
354
|
-
}
|
|
355
|
-
if (wrote.config) process.stdout.write("✓ wrote open-classify.config.json\n");
|
|
356
|
-
if (wrote.catalog) process.stdout.write(`✓ wrote ${DOWNSTREAM_MODELS_FILENAME}\n`);
|
|
357
|
-
if (wrote.readme || wrote.templateCount > 0) {
|
|
358
|
-
const classifierDirRel = relative(cwd, resolvedClassifierDir);
|
|
359
|
-
if (wrote.templateCount > 0) {
|
|
360
|
-
process.stdout.write(`✓ scaffolded ${wrote.templateCount} classifier(s) in ./${classifierDirRel}/\n`);
|
|
361
|
-
} else {
|
|
362
|
-
process.stdout.write(`✓ wrote ./${classifierDirRel}/README.md\n`);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
148
|
+
const cfg = readScaffoldConfig();
|
|
366
149
|
process.stdout.write(`
|
|
367
150
|
Next steps:
|
|
368
151
|
|
|
369
152
|
1. Pull the default classifier model:
|
|
370
|
-
|
|
371
|
-
ollama pull ${config.runner.defaultModel}
|
|
153
|
+
ollama pull ${cfg.runner.defaultModel}
|
|
372
154
|
|
|
373
155
|
2. Verify everything is wired up:
|
|
374
|
-
|
|
375
156
|
npx open-classify doctor
|
|
376
157
|
|
|
377
|
-
3. Try it without writing
|
|
378
|
-
|
|
158
|
+
3. Try it without writing code:
|
|
379
159
|
npx open-classify try "hello"
|
|
380
160
|
|
|
381
161
|
4. Use it from your code:
|
|
382
|
-
|
|
383
162
|
import { createClassifier } from "open-classify";
|
|
384
163
|
const { classify } = createClassifier();
|
|
385
|
-
const result = await classify({
|
|
386
|
-
messages: [{ role: "user", text: "hello" }],
|
|
387
|
-
});
|
|
388
164
|
|
|
389
|
-
|
|
390
|
-
|
|
165
|
+
createClassifier() finds ./${PROJECT_DIRNAME}/config.json and wires
|
|
166
|
+
in ./${PROJECT_DIRNAME}/classifiers/ automatically.
|
|
391
167
|
|
|
392
168
|
Docs: https://github.com/taylorbayouth/open-classify#readme
|
|
393
169
|
`);
|
|
394
170
|
}
|
|
395
171
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
172
|
+
// Recursively plan a directory copy from source → dest, relative to projectCwd
|
|
173
|
+
// for display. Returns { actions, preview, skipped }.
|
|
174
|
+
function planScaffoldCopy(sourceDir, destDir, projectCwd, force) {
|
|
399
175
|
const actions = [];
|
|
400
176
|
const preview = [];
|
|
177
|
+
const skipped = [];
|
|
401
178
|
|
|
402
|
-
|
|
403
|
-
const configPath = join(cwd, "open-classify.config.json");
|
|
404
|
-
const configRel = relative(cwd, configPath);
|
|
405
|
-
if (existsSync(configPath) && !force) {
|
|
406
|
-
toSkip.push(configRel);
|
|
407
|
-
} else {
|
|
408
|
-
toCreate.push(configRel);
|
|
409
|
-
preview.push({ label: configRel, description: `(default Ollama setup, ${config.runner.defaultModel})` });
|
|
410
|
-
actions.push(() => {
|
|
411
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
412
|
-
process.stdout.write(` wrote ${configRel}\n`);
|
|
413
|
-
wrote.config = true;
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Downstream model catalog.
|
|
418
|
-
const catalogPath = join(cwd, DOWNSTREAM_MODELS_FILENAME);
|
|
419
|
-
const catalogRel = relative(cwd, catalogPath);
|
|
420
|
-
if (existsSync(catalogPath) && !force) {
|
|
421
|
-
toSkip.push(catalogRel);
|
|
422
|
-
} else {
|
|
423
|
-
toCreate.push(catalogRel);
|
|
424
|
-
preview.push({ label: catalogRel, description: "default downstream model catalog" });
|
|
425
|
-
actions.push(() => {
|
|
426
|
-
cpSync(DOWNSTREAM_MODELS_PATH, catalogPath);
|
|
427
|
-
process.stdout.write(` wrote ${catalogRel}\n`);
|
|
428
|
-
wrote.catalog = true;
|
|
429
|
-
});
|
|
430
|
-
}
|
|
179
|
+
walk(sourceDir, destDir);
|
|
431
180
|
|
|
432
|
-
|
|
433
|
-
const classifierPreviewItems = [];
|
|
181
|
+
return { actions, preview, skipped };
|
|
434
182
|
|
|
435
|
-
|
|
436
|
-
if (!existsSync(
|
|
437
|
-
toCreate.push(`${relative(cwd, classifierDir)}/`);
|
|
183
|
+
function walk(src, dst) {
|
|
184
|
+
if (!existsSync(dst)) {
|
|
438
185
|
actions.push(() => {
|
|
439
|
-
mkdirSync(
|
|
186
|
+
mkdirSync(dst, { recursive: true });
|
|
187
|
+
process.stdout.write(` created ${relative(projectCwd, dst)}/\n`);
|
|
440
188
|
});
|
|
189
|
+
preview.push(`${relative(projectCwd, dst)}/`);
|
|
441
190
|
}
|
|
442
191
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
writeFileSync(readmePath, CLASSIFIERS_README);
|
|
454
|
-
process.stdout.write(` wrote ${readmeRel}\n`);
|
|
455
|
-
wrote.readme = true;
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Template classifier directories.
|
|
460
|
-
for (const name of TEMPLATE_NAMES) {
|
|
461
|
-
const inactivePath = join(classifierDir, `_${name}`);
|
|
462
|
-
const activePath = join(classifierDir, name);
|
|
463
|
-
const inactiveRel = relative(cwd, inactivePath);
|
|
464
|
-
const activeRel = relative(cwd, activePath);
|
|
465
|
-
|
|
466
|
-
// Never overwrite an activated (user-renamed) template.
|
|
467
|
-
if (existsSync(activePath)) {
|
|
468
|
-
toSkip.push(`${activeRel}/`);
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
if (existsSync(inactivePath) && !force) {
|
|
473
|
-
toSkip.push(`${inactiveRel}/`);
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
toCreate.push(`${inactiveRel}/`);
|
|
478
|
-
classifierPreviewItems.push({
|
|
479
|
-
label: `_${name}/`,
|
|
480
|
-
indent: true,
|
|
481
|
-
description: TEMPLATE_DESCRIPTIONS[name],
|
|
482
|
-
});
|
|
483
|
-
actions.push(() => {
|
|
484
|
-
mkdirSync(classifierDir, { recursive: true });
|
|
485
|
-
if (force && existsSync(inactivePath)) {
|
|
486
|
-
rmSync(inactivePath, { recursive: true, force: true });
|
|
192
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
193
|
+
const srcChild = join(src, entry.name);
|
|
194
|
+
const dstChild = join(dst, entry.name);
|
|
195
|
+
if (entry.isDirectory()) {
|
|
196
|
+
walk(srcChild, dstChild);
|
|
197
|
+
} else {
|
|
198
|
+
const exists = existsSync(dstChild);
|
|
199
|
+
if (exists && !force) {
|
|
200
|
+
skipped.push(relative(projectCwd, dstChild));
|
|
201
|
+
continue;
|
|
487
202
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (classifierPreviewItems.length > 0) {
|
|
495
|
-
preview.push({ label: `${relative(cwd, classifierDir)}/`, isGroupHeader: true });
|
|
496
|
-
preview.push(...classifierPreviewItems);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
return { toCreate, toSkip, actions, preview };
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function showDiffs(conflicts, cwd, classifierDir, config = DEFAULT_CONFIG) {
|
|
504
|
-
for (const p of conflicts) {
|
|
505
|
-
const isDir = p.endsWith("/");
|
|
506
|
-
const relPath = isDir ? p.slice(0, -1) : p;
|
|
507
|
-
const fullPath = join(cwd, relPath);
|
|
508
|
-
|
|
509
|
-
process.stdout.write(`\n--- ${p} ---\n`);
|
|
510
|
-
|
|
511
|
-
if (!isDir) {
|
|
512
|
-
process.stdout.write("\n current:\n");
|
|
513
|
-
try {
|
|
514
|
-
const lines = readFileSync(fullPath, "utf8").split("\n");
|
|
515
|
-
for (const line of lines) process.stdout.write(` ${line}\n`);
|
|
516
|
-
} catch {
|
|
517
|
-
process.stdout.write(" (could not read)\n");
|
|
518
|
-
}
|
|
519
|
-
process.stdout.write("\n would become:\n");
|
|
520
|
-
const replacement =
|
|
521
|
-
basename(relPath) === DOWNSTREAM_MODELS_FILENAME
|
|
522
|
-
? readFileSync(DOWNSTREAM_MODELS_PATH, "utf8")
|
|
523
|
-
: JSON.stringify(config, null, 2);
|
|
524
|
-
for (const line of replacement.split("\n")) {
|
|
525
|
-
process.stdout.write(` ${line}\n`);
|
|
526
|
-
}
|
|
527
|
-
} else {
|
|
528
|
-
process.stdout.write("\n current files:\n");
|
|
529
|
-
try {
|
|
530
|
-
for (const f of readdirSync(fullPath)) process.stdout.write(` ${f}\n`);
|
|
531
|
-
} catch {
|
|
532
|
-
process.stdout.write(" (could not read)\n");
|
|
533
|
-
}
|
|
534
|
-
const templateName = basename(relPath).replace(/^_/, "");
|
|
535
|
-
const templatePath = join(TEMPLATES_DIR, templateName);
|
|
536
|
-
if (existsSync(templatePath)) {
|
|
537
|
-
process.stdout.write("\n template files:\n");
|
|
538
|
-
for (const f of readdirSync(templatePath)) process.stdout.write(` ${f}\n`);
|
|
203
|
+
actions.push(() => {
|
|
204
|
+
cpSync(srcChild, dstChild);
|
|
205
|
+
process.stdout.write(` wrote ${relative(projectCwd, dstChild)}\n`);
|
|
206
|
+
});
|
|
207
|
+
preview.push(relative(projectCwd, dstChild));
|
|
539
208
|
}
|
|
540
209
|
}
|
|
541
210
|
}
|
|
542
|
-
process.stdout.write("\n");
|
|
543
211
|
}
|
|
544
212
|
|
|
545
213
|
// ---------------------------------------------------------------------------
|
|
546
|
-
//
|
|
214
|
+
// eject
|
|
547
215
|
// ---------------------------------------------------------------------------
|
|
548
216
|
|
|
549
|
-
async function
|
|
550
|
-
|
|
551
|
-
|
|
217
|
+
async function runEject({ cwd, name, yes, force, dryRun }) {
|
|
218
|
+
if (!name) {
|
|
219
|
+
process.stderr.write(`Usage: open-classify eject <name>\n\nAvailable: ${STOCK_NAMES.join(", ")}\n`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
if (!STOCK_NAMES.includes(name)) {
|
|
223
|
+
process.stderr.write(`✖ "${name}" is not a stock classifier.\n\nAvailable: ${STOCK_NAMES.join(", ")}\n`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
552
226
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
}
|
|
560
|
-
return;
|
|
227
|
+
const projectDir = join(cwd, PROJECT_DIRNAME);
|
|
228
|
+
if (!existsSync(projectDir)) {
|
|
229
|
+
process.stderr.write(
|
|
230
|
+
`✖ ./${PROJECT_DIRNAME}/ not found. Run \`npx open-classify init\` first.\n`,
|
|
231
|
+
);
|
|
232
|
+
process.exit(1);
|
|
561
233
|
}
|
|
562
234
|
|
|
563
|
-
|
|
564
|
-
|
|
235
|
+
const source = join(STOCK_DIR, name);
|
|
236
|
+
const dest = join(projectDir, "classifiers", name);
|
|
237
|
+
const destRel = relative(cwd, dest);
|
|
238
|
+
|
|
239
|
+
if (existsSync(dest) && !force) {
|
|
240
|
+
process.stderr.write(
|
|
241
|
+
`✖ ${destRel}/ already exists. Use --force to overwrite, or delete it first.\n`,
|
|
242
|
+
);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
565
245
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
246
|
+
process.stdout.write(`\nEjecting "${name}" → ${destRel}/\n\n`);
|
|
247
|
+
for (const filename of ["manifest.json", "prompt.md"]) {
|
|
248
|
+
const srcFile = join(source, filename);
|
|
249
|
+
const dstFile = join(dest, filename);
|
|
250
|
+
process.stdout.write(` ${relative(cwd, dstFile)}\n`);
|
|
570
251
|
}
|
|
571
252
|
|
|
572
253
|
if (dryRun) {
|
|
573
|
-
process.stdout.write("\n(dry run — nothing
|
|
254
|
+
process.stdout.write("\n(dry run — nothing written)\n");
|
|
574
255
|
return;
|
|
575
256
|
}
|
|
576
257
|
|
|
@@ -582,80 +263,18 @@ async function runUninstall({ cwd, yes, dryRun, force, classifierDir }) {
|
|
|
582
263
|
}
|
|
583
264
|
}
|
|
584
265
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
process.stdout.write("\n✓ removed open-classify scaffold\n");
|
|
588
|
-
process.stdout.write("To remove the package dependency too, run: npm uninstall open-classify\n");
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function planUninstall(cwd, { classifierDir, force }) {
|
|
592
|
-
const toRemove = [];
|
|
593
|
-
const toSkip = [];
|
|
594
|
-
const actions = [];
|
|
266
|
+
mkdirSync(dest, { recursive: true });
|
|
267
|
+
cpSync(source, dest, { recursive: true });
|
|
595
268
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (!existsSync(path)) continue;
|
|
599
|
-
toRemove.push(filename);
|
|
600
|
-
actions.push(() => {
|
|
601
|
-
rmSync(path, { force: true });
|
|
602
|
-
process.stdout.write(` removed ${filename}\n`);
|
|
603
|
-
});
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
const classifierRel = relative(cwd, classifierDir);
|
|
607
|
-
if (!existsSync(classifierDir)) {
|
|
608
|
-
return { toRemove, toSkip, actions };
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
if (force) {
|
|
612
|
-
toRemove.push(`${classifierRel}/`);
|
|
613
|
-
actions.push(() => {
|
|
614
|
-
rmSync(classifierDir, { recursive: true, force: true });
|
|
615
|
-
process.stdout.write(` removed ${classifierRel}/\n`);
|
|
616
|
-
});
|
|
617
|
-
return { toRemove, toSkip, actions };
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
const readmePath = join(classifierDir, "README.md");
|
|
621
|
-
const readmeRel = relative(cwd, readmePath);
|
|
622
|
-
if (existsSync(readmePath)) {
|
|
623
|
-
toRemove.push(readmeRel);
|
|
624
|
-
actions.push(() => {
|
|
625
|
-
rmSync(readmePath, { force: true });
|
|
626
|
-
process.stdout.write(` removed ${readmeRel}\n`);
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
for (const name of TEMPLATE_NAMES) {
|
|
631
|
-
const inactivePath = join(classifierDir, `_${name}`);
|
|
632
|
-
const inactiveRel = relative(cwd, inactivePath);
|
|
633
|
-
if (existsSync(inactivePath)) {
|
|
634
|
-
toRemove.push(`${inactiveRel}/`);
|
|
635
|
-
actions.push(() => {
|
|
636
|
-
rmSync(inactivePath, { recursive: true, force: true });
|
|
637
|
-
process.stdout.write(` removed ${inactiveRel}/\n`);
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
const activePath = join(classifierDir, name);
|
|
642
|
-
if (existsSync(activePath)) {
|
|
643
|
-
toSkip.push(`${relative(cwd, activePath)}/`);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
actions.push(() => {
|
|
648
|
-
try {
|
|
649
|
-
if (readdirSync(classifierDir).length === 0) {
|
|
650
|
-
rmSync(classifierDir, { recursive: true, force: true });
|
|
651
|
-
process.stdout.write(` removed ${classifierRel}/\n`);
|
|
652
|
-
}
|
|
653
|
-
} catch {
|
|
654
|
-
// Non-empty: custom/active classifiers remain, which is intentional.
|
|
655
|
-
}
|
|
656
|
-
});
|
|
269
|
+
process.stdout.write(`
|
|
270
|
+
✓ ejected ${name}
|
|
657
271
|
|
|
658
|
-
|
|
272
|
+
The runtime now uses your local copy at ${destRel}/. Edit prompt.md or
|
|
273
|
+
manifest.json to taste. \`npm update open-classify\` won't touch these
|
|
274
|
+
files. To revert: delete the folder. If you want the package-owned
|
|
275
|
+
version to take over after that, add "${name}" to classifiers.stock in
|
|
276
|
+
${PROJECT_DIRNAME}/config.json.
|
|
277
|
+
`);
|
|
659
278
|
}
|
|
660
279
|
|
|
661
280
|
// ---------------------------------------------------------------------------
|
|
@@ -666,123 +285,111 @@ async function runDoctor({ cwd }) {
|
|
|
666
285
|
let allGood = true;
|
|
667
286
|
|
|
668
287
|
// 1. package.json + open-classify dep.
|
|
669
|
-
const
|
|
670
|
-
if (
|
|
288
|
+
const pkg = readJsonIfExists(join(cwd, "package.json"));
|
|
289
|
+
if (pkg === null) {
|
|
671
290
|
process.stdout.write("✖ No package.json — not a Node project\n");
|
|
672
291
|
allGood = false;
|
|
292
|
+
} else if (isOpenClassifyDep(pkg)) {
|
|
293
|
+
process.stdout.write("✓ open-classify found in package.json\n");
|
|
673
294
|
} else {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (isOpenClassifyDep(pkg)) {
|
|
677
|
-
process.stdout.write("✓ open-classify found in package.json\n");
|
|
678
|
-
} else {
|
|
679
|
-
process.stdout.write("⚠ open-classify not listed as a dependency\n");
|
|
680
|
-
allGood = false;
|
|
681
|
-
}
|
|
295
|
+
process.stdout.write("⚠ open-classify not listed as a dependency — run: npm install open-classify\n");
|
|
296
|
+
allGood = false;
|
|
682
297
|
}
|
|
683
298
|
|
|
684
|
-
// 2. Config parses.
|
|
685
|
-
const
|
|
299
|
+
// 2. Config parses + catalog present.
|
|
300
|
+
const projectDir = join(cwd, PROJECT_DIRNAME);
|
|
301
|
+
const configPath = join(projectDir, "config.json");
|
|
302
|
+
let config = null;
|
|
686
303
|
if (!existsSync(configPath)) {
|
|
687
|
-
process.stdout.write(
|
|
304
|
+
process.stdout.write(`✖ ./${PROJECT_DIRNAME}/config.json not found — run: npx open-classify init\n`);
|
|
688
305
|
allGood = false;
|
|
689
306
|
} else {
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
config
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const
|
|
697
|
-
const catalogPath = resolve(
|
|
307
|
+
config = readJsonIfExists(configPath);
|
|
308
|
+
if (config === null) {
|
|
309
|
+
process.stdout.write(`✖ ./${PROJECT_DIRNAME}/config.json is not valid JSON\n`);
|
|
310
|
+
allGood = false;
|
|
311
|
+
} else {
|
|
312
|
+
process.stdout.write(`✓ ./${PROJECT_DIRNAME}/config.json parses OK\n`);
|
|
313
|
+
const catalogRel = config.catalog ?? "downstream-models.json";
|
|
314
|
+
const catalogPath = resolve(projectDir, catalogRel);
|
|
698
315
|
if (existsSync(catalogPath)) {
|
|
699
|
-
process.stdout.write(`✓ ${
|
|
316
|
+
process.stdout.write(`✓ catalog found at ${relative(cwd, catalogPath)}\n`);
|
|
700
317
|
} else {
|
|
701
|
-
process.stdout.write(`✖
|
|
318
|
+
process.stdout.write(`✖ catalog not found at ${relative(cwd, catalogPath)}\n`);
|
|
702
319
|
allGood = false;
|
|
703
320
|
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
704
323
|
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
process.stdout.write(`✓ Model ${model} is available\n`);
|
|
718
|
-
} else {
|
|
719
|
-
process.stdout.write(`✖ Model ${model} not found — run: ollama pull ${model}\n`);
|
|
720
|
-
allGood = false;
|
|
721
|
-
}
|
|
324
|
+
// 3. Ollama reachable + default model pulled.
|
|
325
|
+
if (config !== null) {
|
|
326
|
+
const host = config.runner?.host ?? "http://127.0.0.1:11434";
|
|
327
|
+
const defaultModel = config.runner?.defaultModel ?? "gemma4:e4b-it-q4_K_M";
|
|
328
|
+
try {
|
|
329
|
+
const res = await fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(3000) });
|
|
330
|
+
if (res.ok) {
|
|
331
|
+
process.stdout.write(`✓ Ollama reachable at ${host}\n`);
|
|
332
|
+
const data = await res.json();
|
|
333
|
+
const pulled = data.models?.some((m) => m.name === defaultModel || m.model === defaultModel);
|
|
334
|
+
if (pulled) {
|
|
335
|
+
process.stdout.write(`✓ Model ${defaultModel} is available\n`);
|
|
722
336
|
} else {
|
|
723
|
-
process.stdout.write(`✖
|
|
337
|
+
process.stdout.write(`✖ Model ${defaultModel} not found — run: ollama pull ${defaultModel}\n`);
|
|
724
338
|
allGood = false;
|
|
725
339
|
}
|
|
726
|
-
}
|
|
727
|
-
process.stdout.write(`✖ Ollama
|
|
340
|
+
} else {
|
|
341
|
+
process.stdout.write(`✖ Ollama responded ${res.status} at ${host}\n`);
|
|
728
342
|
allGood = false;
|
|
729
343
|
}
|
|
730
344
|
} catch {
|
|
731
|
-
process.stdout.write(
|
|
345
|
+
process.stdout.write(`✖ Ollama not reachable at ${host} — is it running?\n`);
|
|
732
346
|
allGood = false;
|
|
733
347
|
}
|
|
734
348
|
}
|
|
735
349
|
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
350
|
+
// 4. Classifier directories.
|
|
351
|
+
if (config !== null) {
|
|
352
|
+
const dirs = config.classifiers?.dirs ?? ["classifiers"];
|
|
353
|
+
for (const dirRel of dirs) {
|
|
354
|
+
const dir = resolve(projectDir, dirRel);
|
|
355
|
+
const displayRel = relative(cwd, dir);
|
|
356
|
+
if (!existsSync(dir)) {
|
|
357
|
+
process.stdout.write(`ℹ No ${displayRel}/ — run: npx open-classify init\n`);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
let active = 0;
|
|
361
|
+
let bad = 0;
|
|
362
|
+
try {
|
|
363
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
364
|
+
if (!entry.isDirectory() || entry.name.startsWith("_")) continue;
|
|
365
|
+
const sub = join(dir, entry.name);
|
|
366
|
+
const ok = existsSync(join(sub, "manifest.json")) && existsSync(join(sub, "prompt.md"));
|
|
367
|
+
if (ok) {
|
|
368
|
+
active++;
|
|
369
|
+
} else {
|
|
370
|
+
process.stdout.write(`✖ ${displayRel}/${entry.name}/ is missing manifest.json or prompt.md\n`);
|
|
371
|
+
bad++;
|
|
372
|
+
allGood = false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
} catch {/* skip */}
|
|
376
|
+
if (bad === 0) {
|
|
377
|
+
const stockEnabled = (config.classifiers?.stock ?? []).length;
|
|
378
|
+
process.stdout.write(
|
|
379
|
+
active > 0
|
|
380
|
+
? `✓ ${active} user classifier(s) in ${displayRel}/\n`
|
|
381
|
+
: `ℹ No user classifiers in ${displayRel}/${stockEnabled > 0 ? "" : " (use `npx open-classify eject <name>` to customize a stock classifier)"}\n`,
|
|
382
|
+
);
|
|
383
|
+
if (stockEnabled > 0) {
|
|
384
|
+
process.stdout.write(`✓ ${stockEnabled} stock classifier(s) enabled in config\n`);
|
|
763
385
|
}
|
|
764
386
|
}
|
|
765
|
-
} catch { /* skip */ }
|
|
766
|
-
if (bad === 0) {
|
|
767
|
-
process.stdout.write(
|
|
768
|
-
active > 0
|
|
769
|
-
? `✓ ${active} active classifier(s) in ${classifiersRel}/\n`
|
|
770
|
-
: `ℹ No active classifiers in ${classifiersRel}/ (enable stock in config or customize a _name template)\n`,
|
|
771
|
-
);
|
|
772
387
|
}
|
|
773
388
|
}
|
|
774
389
|
|
|
775
390
|
if (!allGood) process.exit(1);
|
|
776
391
|
}
|
|
777
392
|
|
|
778
|
-
function configFromFile(cwd) {
|
|
779
|
-
try {
|
|
780
|
-
return JSON.parse(readFileSync(join(cwd, "open-classify.config.json"), "utf8"));
|
|
781
|
-
} catch {
|
|
782
|
-
return null;
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
393
|
// ---------------------------------------------------------------------------
|
|
787
394
|
// try
|
|
788
395
|
// ---------------------------------------------------------------------------
|
|
@@ -793,14 +400,12 @@ async function runTry({ cwd, message }) {
|
|
|
793
400
|
process.exit(1);
|
|
794
401
|
}
|
|
795
402
|
|
|
796
|
-
const configPath = join(cwd, "
|
|
403
|
+
const configPath = join(cwd, PROJECT_DIRNAME, "config.json");
|
|
797
404
|
if (!existsSync(configPath)) {
|
|
798
|
-
process.stderr.write(
|
|
405
|
+
process.stderr.write(`✖ ./${PROJECT_DIRNAME}/config.json not found — run: npx open-classify init\n`);
|
|
799
406
|
process.exit(1);
|
|
800
407
|
}
|
|
801
408
|
|
|
802
|
-
// Try loading from the consumer's node_modules first, then fall back to the
|
|
803
|
-
// package root (useful when running from the development checkout).
|
|
804
409
|
let createClassifier;
|
|
805
410
|
const candidates = [
|
|
806
411
|
join(cwd, "node_modules", "open-classify", "dist", "src", "index.js"),
|
|
@@ -809,10 +414,9 @@ async function runTry({ cwd, message }) {
|
|
|
809
414
|
for (const candidate of candidates) {
|
|
810
415
|
if (!existsSync(candidate)) continue;
|
|
811
416
|
try {
|
|
812
|
-
|
|
813
|
-
createClassifier = mod.createClassifier;
|
|
417
|
+
({ createClassifier } = await import(candidate));
|
|
814
418
|
break;
|
|
815
|
-
} catch {
|
|
419
|
+
} catch {/* try next */}
|
|
816
420
|
}
|
|
817
421
|
|
|
818
422
|
if (!createClassifier) {
|
|
@@ -823,17 +427,9 @@ async function runTry({ cwd, message }) {
|
|
|
823
427
|
process.exit(1);
|
|
824
428
|
}
|
|
825
429
|
|
|
826
|
-
const classifiersDir = join(cwd, "classifiers");
|
|
827
430
|
let classifier;
|
|
828
431
|
try {
|
|
829
|
-
|
|
830
|
-
const hasConfiguredClassifierDirs = Array.isArray(config?.classifiers?.dirs);
|
|
831
|
-
classifier = createClassifier({
|
|
832
|
-
configPath,
|
|
833
|
-
extraClassifierDirs:
|
|
834
|
-
hasConfiguredClassifierDirs || !existsSync(classifiersDir) ? [] : [classifiersDir],
|
|
835
|
-
skipResourceCheck: false,
|
|
836
|
-
});
|
|
432
|
+
classifier = createClassifier({ configPath });
|
|
837
433
|
} catch (err) {
|
|
838
434
|
process.stderr.write(`✖ Failed to initialise classifier: ${err.message}\n`);
|
|
839
435
|
process.exit(1);
|
|
@@ -851,30 +447,52 @@ async function runTry({ cwd, message }) {
|
|
|
851
447
|
}
|
|
852
448
|
|
|
853
449
|
// ---------------------------------------------------------------------------
|
|
854
|
-
//
|
|
450
|
+
// Shared helpers
|
|
855
451
|
// ---------------------------------------------------------------------------
|
|
856
452
|
|
|
857
|
-
function
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
}
|
|
453
|
+
function requireHostProject(cwd) {
|
|
454
|
+
if (!existsSync(join(cwd, "package.json"))) {
|
|
455
|
+
process.stderr.write(
|
|
456
|
+
`✖ No package.json found in ${cwd}.\n` +
|
|
457
|
+
` open-classify scaffolds into a Node project, so it needs one to live in.\n\n` +
|
|
458
|
+
` Create one first: npm init -y\n`,
|
|
459
|
+
);
|
|
460
|
+
process.exit(1);
|
|
461
|
+
}
|
|
866
462
|
}
|
|
867
463
|
|
|
868
|
-
function
|
|
869
|
-
|
|
870
|
-
|
|
464
|
+
function warnIfPackageMissing(cwd) {
|
|
465
|
+
const pkg = readJsonIfExists(join(cwd, "package.json"));
|
|
466
|
+
if (pkg === null || isOpenClassifyDep(pkg)) return;
|
|
467
|
+
process.stdout.write(
|
|
468
|
+
`\n⚠ open-classify is not yet a dependency of this project.\n` +
|
|
469
|
+
` Install it before importing from your code:\n\n` +
|
|
470
|
+
` npm install open-classify\n`,
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function readJsonIfExists(path) {
|
|
475
|
+
if (!existsSync(path)) return null;
|
|
476
|
+
try { return JSON.parse(readFileSync(path, "utf8")); } catch { return null; }
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function isOpenClassifyDep(pkg) {
|
|
480
|
+
return ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"].some(
|
|
481
|
+
(f) => pkg[f]?.["open-classify"],
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function readScaffoldConfig() {
|
|
486
|
+
return JSON.parse(readFileSync(join(SCAFFOLD_DIR, "config.json"), "utf8"));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function confirm(prompt, defaultYes = false) {
|
|
490
|
+
return new Promise((resolveAnswer) => {
|
|
871
491
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
872
|
-
rl.question(
|
|
492
|
+
rl.question(prompt, (answer) => {
|
|
873
493
|
rl.close();
|
|
874
494
|
const v = (answer || "").trim().toLowerCase();
|
|
875
|
-
|
|
876
|
-
else if (v === "diff") resolve("diff");
|
|
877
|
-
else resolve("N");
|
|
495
|
+
resolveAnswer(defaultYes ? (v === "" || v === "y" || v === "yes") : (v === "y" || v === "yes"));
|
|
878
496
|
});
|
|
879
497
|
});
|
|
880
498
|
}
|