open-classify 0.9.3 → 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 +265 -682
- 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,182 +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
|
-
keepPackage: false,
|
|
164
|
-
packageManager: null,
|
|
165
|
-
classifierDir: "classifiers",
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
for (let i = 0; i < args.length; i++) {
|
|
169
|
-
const arg = args[i];
|
|
170
|
-
if (arg === "--yes" || arg === "-y") flags.yes = true;
|
|
171
65
|
else if (arg === "--dry-run") flags.dryRun = true;
|
|
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];
|
|
176
|
-
else if (arg === "--classifier-dir" && args[i + 1]) flags.classifierDir = args[++i];
|
|
177
|
-
else if (arg.startsWith("--classifier-dir=")) flags.classifierDir = arg.split("=")[1];
|
|
178
66
|
}
|
|
179
|
-
|
|
180
67
|
return flags;
|
|
181
68
|
}
|
|
182
69
|
|
|
@@ -184,164 +71,69 @@ function printHelp() {
|
|
|
184
71
|
process.stdout.write(`open-classify — runtime CLI
|
|
185
72
|
|
|
186
73
|
Commands:
|
|
187
|
-
init
|
|
188
|
-
|
|
189
|
-
|
|
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.
|
|
194
|
-
|
|
195
|
-
doctor Check that the install, config, Ollama, and classifiers are
|
|
196
|
-
all working. Exits non-zero on failure.
|
|
197
|
-
|
|
198
|
-
try <message> Run the pipeline against a single message and print the
|
|
199
|
-
result. Useful for verifying your setup without touching
|
|
200
|
-
application code.
|
|
201
|
-
|
|
202
|
-
Options for init:
|
|
203
|
-
--minimal Write runtime config/catalog only; skip classifiers/
|
|
204
|
-
--dry-run Preview what would be created; don't write anything
|
|
205
|
-
--force Overwrite existing files without prompting
|
|
206
|
-
--no-install Skip the "add to package.json" prompt
|
|
207
|
-
--package-manager <m> npm | pnpm | yarn | bun (default: auto-detect)
|
|
208
|
-
--classifier-dir <p> Directory for classifiers (default: ./classifiers)
|
|
209
|
-
--yes, -y Accept all prompts (CI mode)
|
|
210
|
-
|
|
211
|
-
Options for uninstall:
|
|
212
|
-
--dry-run Preview what would be removed; don't delete anything
|
|
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)
|
|
216
|
-
--classifier-dir <p> Directory for classifiers (default: ./classifiers)
|
|
217
|
-
--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.
|
|
218
76
|
|
|
219
|
-
|
|
220
|
-
|
|
77
|
+
eject <name> Copy a stock classifier into ./open-classify/classifiers/<name>/
|
|
78
|
+
so you can edit it. Stock classifiers:
|
|
79
|
+
${STOCK_NAMES.join(", ")}
|
|
221
80
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
|
|
225
|
-
if (existsSync(join(cwd, "bun.lockb"))) return "bun";
|
|
226
|
-
return "npm";
|
|
227
|
-
}
|
|
81
|
+
doctor Verify install, config, Ollama, and classifiers.
|
|
82
|
+
Exits non-zero on failure.
|
|
228
83
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
(f) => pkg[f]?.["open-classify"],
|
|
232
|
-
);
|
|
233
|
-
}
|
|
84
|
+
try <message> Run the pipeline against a single message and print
|
|
85
|
+
the result.
|
|
234
86
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
`);
|
|
241
102
|
}
|
|
242
103
|
|
|
243
104
|
// ---------------------------------------------------------------------------
|
|
244
105
|
// init
|
|
245
106
|
// ---------------------------------------------------------------------------
|
|
246
107
|
|
|
247
|
-
async function runInit({ cwd, yes,
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if (!existsSync(pkgPath)) {
|
|
251
|
-
process.stderr.write(
|
|
252
|
-
`✖ No package.json found in ${cwd}.\n` +
|
|
253
|
-
` open-classify scaffolds code that imports the library, so it needs a\n` +
|
|
254
|
-
` Node project to live in.\n\n` +
|
|
255
|
-
` Create one first: npm init -y\n`,
|
|
256
|
-
);
|
|
257
|
-
process.exit(1);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
let pkg;
|
|
261
|
-
try {
|
|
262
|
-
pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
263
|
-
} catch {
|
|
264
|
-
process.stderr.write(`✖ Could not parse package.json at ${pkgPath}\n`);
|
|
265
|
-
process.exit(1);
|
|
266
|
-
}
|
|
108
|
+
async function runInit({ cwd, yes, force, dryRun }) {
|
|
109
|
+
requireHostProject(cwd);
|
|
110
|
+
warnIfPackageMissing(cwd);
|
|
267
111
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (!isOpenClassifyDep(pkg) && !noInstall && !yes) {
|
|
271
|
-
process.stdout.write(`ℹ open-classify is not yet a dependency of this project.\n\n`);
|
|
272
|
-
const doInstall = await confirm("? Add open-classify to package.json and install it now? (Y/n) ", true);
|
|
273
|
-
if (doInstall) {
|
|
274
|
-
const pm = packageManager || detectPackageManager(cwd);
|
|
275
|
-
const installCmd = pm === "npm" ? ["install", "open-classify"] : ["add", "open-classify"];
|
|
276
|
-
process.stdout.write(`\n Running: ${pm} ${installCmd.join(" ")}\n\n`);
|
|
277
|
-
const result = spawnSync(pm, installCmd, { cwd, stdio: "inherit" });
|
|
278
|
-
if (result.status !== 0) {
|
|
279
|
-
process.stderr.write(`\n✖ Install failed. Run manually: ${pm} ${installCmd.join(" ")}\n`);
|
|
280
|
-
process.exit(1);
|
|
281
|
-
}
|
|
282
|
-
installedNow = true;
|
|
283
|
-
process.stdout.write("\n");
|
|
284
|
-
} else {
|
|
285
|
-
process.stdout.write(
|
|
286
|
-
` Skipped. You'll need to run \`npm install open-classify\` before importing.\n\n`,
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
112
|
+
const destRoot = join(cwd, PROJECT_DIRNAME);
|
|
113
|
+
const plan = planScaffoldCopy(SCAFFOLD_DIR, destRoot, cwd, force);
|
|
290
114
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
// Nothing to do.
|
|
298
|
-
if (plan.toCreate.length === 0) {
|
|
299
|
-
process.stdout.write("Nothing to do — your project already has all the scaffolded files.\n");
|
|
300
|
-
if (plan.toSkip.length > 0) {
|
|
301
|
-
process.stdout.write("\nAlready in place:\n");
|
|
302
|
-
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`);
|
|
303
120
|
}
|
|
304
121
|
return;
|
|
305
122
|
}
|
|
306
123
|
|
|
307
|
-
// 4. Preview.
|
|
308
124
|
process.stdout.write(`\nThe following will be created in ${cwd}:\n\n`);
|
|
309
|
-
for (const item of plan.preview) {
|
|
310
|
-
if (item.isGroupHeader) {
|
|
311
|
-
process.stdout.write(` ${item.label}\n`);
|
|
312
|
-
} else if (item.indent) {
|
|
313
|
-
process.stdout.write(` ${item.label.padEnd(32)} ${item.description}\n`);
|
|
314
|
-
} else {
|
|
315
|
-
process.stdout.write(` ${item.label.padEnd(34)} ${item.description}\n`);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
125
|
+
for (const item of plan.preview) process.stdout.write(` ${item}\n`);
|
|
318
126
|
|
|
319
|
-
if (plan.
|
|
320
|
-
process.stdout.write(`\
|
|
321
|
-
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`);
|
|
322
130
|
}
|
|
323
131
|
|
|
324
|
-
// 5. Stop here on --dry-run.
|
|
325
132
|
if (dryRun) {
|
|
326
133
|
process.stdout.write("\n(dry run — nothing written)\n");
|
|
327
134
|
return;
|
|
328
135
|
}
|
|
329
136
|
|
|
330
|
-
// 6. Conflict handling: interactive only (not --yes, not --force).
|
|
331
|
-
if (plan.toSkip.length > 0 && !yes && !force) {
|
|
332
|
-
const choice = await promptConflict();
|
|
333
|
-
if (choice === "diff") {
|
|
334
|
-
showDiffs(plan.toSkip, cwd, resolvedClassifierDir, config);
|
|
335
|
-
const choice2 = await promptConflict();
|
|
336
|
-
if (choice2 === "y") {
|
|
337
|
-
plan = planInit(cwd, { minimal, classifierDir: resolvedClassifierDir, force: true, wrote, config });
|
|
338
|
-
}
|
|
339
|
-
} else if (choice === "y") {
|
|
340
|
-
plan = planInit(cwd, { minimal, classifierDir: resolvedClassifierDir, force: true, wrote, config });
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// 7. Confirm (skip in --yes mode).
|
|
345
137
|
if (!yes) {
|
|
346
138
|
const proceed = await confirm("\n? Continue? (Y/n) ", true);
|
|
347
139
|
if (!proceed) {
|
|
@@ -350,251 +142,116 @@ async function runInit({ cwd, yes, minimal, dryRun, force, noInstall, packageMan
|
|
|
350
142
|
}
|
|
351
143
|
}
|
|
352
144
|
|
|
353
|
-
// 8. Execute.
|
|
354
145
|
process.stdout.write("\n");
|
|
355
146
|
for (const action of plan.actions) action();
|
|
356
147
|
|
|
357
|
-
|
|
358
|
-
process.stdout.write("\n");
|
|
359
|
-
if (installedNow) {
|
|
360
|
-
const v = getCliVersion();
|
|
361
|
-
process.stdout.write(`✓ open-classify installed${v ? ` (v${v})` : ""}\n`);
|
|
362
|
-
}
|
|
363
|
-
if (wrote.config) process.stdout.write("✓ wrote open-classify.config.json\n");
|
|
364
|
-
if (wrote.catalog) process.stdout.write(`✓ wrote ${DOWNSTREAM_MODELS_FILENAME}\n`);
|
|
365
|
-
if (wrote.readme || wrote.templateCount > 0) {
|
|
366
|
-
const classifierDirRel = relative(cwd, resolvedClassifierDir);
|
|
367
|
-
if (wrote.templateCount > 0) {
|
|
368
|
-
process.stdout.write(`✓ scaffolded ${wrote.templateCount} classifier(s) in ./${classifierDirRel}/\n`);
|
|
369
|
-
} else {
|
|
370
|
-
process.stdout.write(`✓ wrote ./${classifierDirRel}/README.md\n`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
148
|
+
const cfg = readScaffoldConfig();
|
|
374
149
|
process.stdout.write(`
|
|
375
150
|
Next steps:
|
|
376
151
|
|
|
377
152
|
1. Pull the default classifier model:
|
|
378
|
-
|
|
379
|
-
ollama pull ${config.runner.defaultModel}
|
|
153
|
+
ollama pull ${cfg.runner.defaultModel}
|
|
380
154
|
|
|
381
155
|
2. Verify everything is wired up:
|
|
382
|
-
|
|
383
156
|
npx open-classify doctor
|
|
384
157
|
|
|
385
|
-
3. Try it without writing
|
|
386
|
-
|
|
158
|
+
3. Try it without writing code:
|
|
387
159
|
npx open-classify try "hello"
|
|
388
160
|
|
|
389
161
|
4. Use it from your code:
|
|
390
|
-
|
|
391
162
|
import { createClassifier } from "open-classify";
|
|
392
163
|
const { classify } = createClassifier();
|
|
393
|
-
const result = await classify({
|
|
394
|
-
messages: [{ role: "user", text: "hello" }],
|
|
395
|
-
});
|
|
396
164
|
|
|
397
|
-
|
|
398
|
-
|
|
165
|
+
createClassifier() finds ./${PROJECT_DIRNAME}/config.json and wires
|
|
166
|
+
in ./${PROJECT_DIRNAME}/classifiers/ automatically.
|
|
399
167
|
|
|
400
168
|
Docs: https://github.com/taylorbayouth/open-classify#readme
|
|
401
169
|
`);
|
|
402
170
|
}
|
|
403
171
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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) {
|
|
407
175
|
const actions = [];
|
|
408
176
|
const preview = [];
|
|
177
|
+
const skipped = [];
|
|
409
178
|
|
|
410
|
-
|
|
411
|
-
const configPath = join(cwd, "open-classify.config.json");
|
|
412
|
-
const configRel = relative(cwd, configPath);
|
|
413
|
-
if (existsSync(configPath) && !force) {
|
|
414
|
-
toSkip.push(configRel);
|
|
415
|
-
} else {
|
|
416
|
-
toCreate.push(configRel);
|
|
417
|
-
preview.push({ label: configRel, description: `(default Ollama setup, ${config.runner.defaultModel})` });
|
|
418
|
-
actions.push(() => {
|
|
419
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
420
|
-
process.stdout.write(` wrote ${configRel}\n`);
|
|
421
|
-
wrote.config = true;
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Downstream model catalog.
|
|
426
|
-
const catalogPath = join(cwd, DOWNSTREAM_MODELS_FILENAME);
|
|
427
|
-
const catalogRel = relative(cwd, catalogPath);
|
|
428
|
-
if (existsSync(catalogPath) && !force) {
|
|
429
|
-
toSkip.push(catalogRel);
|
|
430
|
-
} else {
|
|
431
|
-
toCreate.push(catalogRel);
|
|
432
|
-
preview.push({ label: catalogRel, description: "default downstream model catalog" });
|
|
433
|
-
actions.push(() => {
|
|
434
|
-
cpSync(DOWNSTREAM_MODELS_PATH, catalogPath);
|
|
435
|
-
process.stdout.write(` wrote ${catalogRel}\n`);
|
|
436
|
-
wrote.catalog = true;
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (!minimal) {
|
|
441
|
-
const classifierPreviewItems = [];
|
|
179
|
+
walk(sourceDir, destDir);
|
|
442
180
|
|
|
443
|
-
|
|
444
|
-
if (!existsSync(classifierDir)) {
|
|
445
|
-
toCreate.push(`${relative(cwd, classifierDir)}/`);
|
|
446
|
-
actions.push(() => {
|
|
447
|
-
mkdirSync(classifierDir, { recursive: true });
|
|
448
|
-
});
|
|
449
|
-
}
|
|
181
|
+
return { actions, preview, skipped };
|
|
450
182
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const readmeRel = relative(cwd, readmePath);
|
|
454
|
-
if (existsSync(readmePath) && !force) {
|
|
455
|
-
toSkip.push(readmeRel);
|
|
456
|
-
} else {
|
|
457
|
-
toCreate.push(readmeRel);
|
|
458
|
-
classifierPreviewItems.push({ label: "README.md", indent: true, description: "how to author your own classifier" });
|
|
183
|
+
function walk(src, dst) {
|
|
184
|
+
if (!existsSync(dst)) {
|
|
459
185
|
actions.push(() => {
|
|
460
|
-
mkdirSync(
|
|
461
|
-
|
|
462
|
-
process.stdout.write(` wrote ${readmeRel}\n`);
|
|
463
|
-
wrote.readme = true;
|
|
186
|
+
mkdirSync(dst, { recursive: true });
|
|
187
|
+
process.stdout.write(` created ${relative(projectCwd, dst)}/\n`);
|
|
464
188
|
});
|
|
189
|
+
preview.push(`${relative(projectCwd, dst)}/`);
|
|
465
190
|
}
|
|
466
191
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (existsSync(inactivePath) && !force) {
|
|
481
|
-
toSkip.push(`${inactiveRel}/`);
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
toCreate.push(`${inactiveRel}/`);
|
|
486
|
-
classifierPreviewItems.push({
|
|
487
|
-
label: `_${name}/`,
|
|
488
|
-
indent: true,
|
|
489
|
-
description: TEMPLATE_DESCRIPTIONS[name],
|
|
490
|
-
});
|
|
491
|
-
actions.push(() => {
|
|
492
|
-
mkdirSync(classifierDir, { recursive: true });
|
|
493
|
-
if (force && existsSync(inactivePath)) {
|
|
494
|
-
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;
|
|
495
202
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (classifierPreviewItems.length > 0) {
|
|
503
|
-
preview.push({ label: `${relative(cwd, classifierDir)}/`, isGroupHeader: true });
|
|
504
|
-
preview.push(...classifierPreviewItems);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return { toCreate, toSkip, actions, preview };
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
function showDiffs(conflicts, cwd, classifierDir, config = DEFAULT_CONFIG) {
|
|
512
|
-
for (const p of conflicts) {
|
|
513
|
-
const isDir = p.endsWith("/");
|
|
514
|
-
const relPath = isDir ? p.slice(0, -1) : p;
|
|
515
|
-
const fullPath = join(cwd, relPath);
|
|
516
|
-
|
|
517
|
-
process.stdout.write(`\n--- ${p} ---\n`);
|
|
518
|
-
|
|
519
|
-
if (!isDir) {
|
|
520
|
-
process.stdout.write("\n current:\n");
|
|
521
|
-
try {
|
|
522
|
-
const lines = readFileSync(fullPath, "utf8").split("\n");
|
|
523
|
-
for (const line of lines) process.stdout.write(` ${line}\n`);
|
|
524
|
-
} catch {
|
|
525
|
-
process.stdout.write(" (could not read)\n");
|
|
526
|
-
}
|
|
527
|
-
process.stdout.write("\n would become:\n");
|
|
528
|
-
const replacement =
|
|
529
|
-
basename(relPath) === DOWNSTREAM_MODELS_FILENAME
|
|
530
|
-
? readFileSync(DOWNSTREAM_MODELS_PATH, "utf8")
|
|
531
|
-
: JSON.stringify(config, null, 2);
|
|
532
|
-
for (const line of replacement.split("\n")) {
|
|
533
|
-
process.stdout.write(` ${line}\n`);
|
|
534
|
-
}
|
|
535
|
-
} else {
|
|
536
|
-
process.stdout.write("\n current files:\n");
|
|
537
|
-
try {
|
|
538
|
-
for (const f of readdirSync(fullPath)) process.stdout.write(` ${f}\n`);
|
|
539
|
-
} catch {
|
|
540
|
-
process.stdout.write(" (could not read)\n");
|
|
541
|
-
}
|
|
542
|
-
const templateName = basename(relPath).replace(/^_/, "");
|
|
543
|
-
const templatePath = join(TEMPLATES_DIR, templateName);
|
|
544
|
-
if (existsSync(templatePath)) {
|
|
545
|
-
process.stdout.write("\n template files:\n");
|
|
546
|
-
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));
|
|
547
208
|
}
|
|
548
209
|
}
|
|
549
210
|
}
|
|
550
|
-
process.stdout.write("\n");
|
|
551
211
|
}
|
|
552
212
|
|
|
553
213
|
// ---------------------------------------------------------------------------
|
|
554
|
-
//
|
|
214
|
+
// eject
|
|
555
215
|
// ---------------------------------------------------------------------------
|
|
556
216
|
|
|
557
|
-
async function
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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; }
|
|
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);
|
|
565
221
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if (plan.toRemove.length === 0 && !willRemovePackage) {
|
|
571
|
-
process.stdout.write("Nothing to remove — no open-classify scaffold or dependency found.\n");
|
|
572
|
-
if (plan.toSkip.length > 0) {
|
|
573
|
-
process.stdout.write("\nSkipped active/custom classifier dirs:\n");
|
|
574
|
-
for (const p of plan.toSkip) process.stdout.write(` ${p}\n`);
|
|
575
|
-
process.stdout.write("\nUse --force to remove the whole classifiers/ directory.\n");
|
|
576
|
-
}
|
|
577
|
-
return;
|
|
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);
|
|
578
225
|
}
|
|
579
226
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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);
|
|
584
233
|
}
|
|
585
234
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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);
|
|
590
244
|
}
|
|
591
245
|
|
|
592
|
-
|
|
593
|
-
|
|
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`);
|
|
594
251
|
}
|
|
595
252
|
|
|
596
253
|
if (dryRun) {
|
|
597
|
-
process.stdout.write("\n(dry run — nothing
|
|
254
|
+
process.stdout.write("\n(dry run — nothing written)\n");
|
|
598
255
|
return;
|
|
599
256
|
}
|
|
600
257
|
|
|
@@ -606,91 +263,18 @@ async function runUninstall({ cwd, yes, dryRun, force, keepPackage, packageManag
|
|
|
606
263
|
}
|
|
607
264
|
}
|
|
608
265
|
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function planUninstall(cwd, { classifierDir, force }) {
|
|
627
|
-
const toRemove = [];
|
|
628
|
-
const toSkip = [];
|
|
629
|
-
const actions = [];
|
|
630
|
-
|
|
631
|
-
for (const filename of ["open-classify.config.json", DOWNSTREAM_MODELS_FILENAME]) {
|
|
632
|
-
const path = join(cwd, filename);
|
|
633
|
-
if (!existsSync(path)) continue;
|
|
634
|
-
toRemove.push(filename);
|
|
635
|
-
actions.push(() => {
|
|
636
|
-
rmSync(path, { force: true });
|
|
637
|
-
process.stdout.write(` removed ${filename}\n`);
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
const classifierRel = relative(cwd, classifierDir);
|
|
642
|
-
if (!existsSync(classifierDir)) {
|
|
643
|
-
return { toRemove, toSkip, actions };
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
if (force) {
|
|
647
|
-
toRemove.push(`${classifierRel}/`);
|
|
648
|
-
actions.push(() => {
|
|
649
|
-
rmSync(classifierDir, { recursive: true, force: true });
|
|
650
|
-
process.stdout.write(` removed ${classifierRel}/\n`);
|
|
651
|
-
});
|
|
652
|
-
return { toRemove, toSkip, actions };
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const readmePath = join(classifierDir, "README.md");
|
|
656
|
-
const readmeRel = relative(cwd, readmePath);
|
|
657
|
-
if (existsSync(readmePath)) {
|
|
658
|
-
toRemove.push(readmeRel);
|
|
659
|
-
actions.push(() => {
|
|
660
|
-
rmSync(readmePath, { force: true });
|
|
661
|
-
process.stdout.write(` removed ${readmeRel}\n`);
|
|
662
|
-
});
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
for (const name of TEMPLATE_NAMES) {
|
|
666
|
-
const inactivePath = join(classifierDir, `_${name}`);
|
|
667
|
-
const inactiveRel = relative(cwd, inactivePath);
|
|
668
|
-
if (existsSync(inactivePath)) {
|
|
669
|
-
toRemove.push(`${inactiveRel}/`);
|
|
670
|
-
actions.push(() => {
|
|
671
|
-
rmSync(inactivePath, { recursive: true, force: true });
|
|
672
|
-
process.stdout.write(` removed ${inactiveRel}/\n`);
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const activePath = join(classifierDir, name);
|
|
677
|
-
if (existsSync(activePath)) {
|
|
678
|
-
toSkip.push(`${relative(cwd, activePath)}/`);
|
|
679
|
-
}
|
|
680
|
-
}
|
|
266
|
+
mkdirSync(dest, { recursive: true });
|
|
267
|
+
cpSync(source, dest, { recursive: true });
|
|
681
268
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
if (readdirSync(classifierDir).length === 0) {
|
|
685
|
-
rmSync(classifierDir, { recursive: true, force: true });
|
|
686
|
-
process.stdout.write(` removed ${classifierRel}/\n`);
|
|
687
|
-
}
|
|
688
|
-
} catch {
|
|
689
|
-
// Non-empty: custom/active classifiers remain, which is intentional.
|
|
690
|
-
}
|
|
691
|
-
});
|
|
269
|
+
process.stdout.write(`
|
|
270
|
+
✓ ejected ${name}
|
|
692
271
|
|
|
693
|
-
|
|
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
|
+
`);
|
|
694
278
|
}
|
|
695
279
|
|
|
696
280
|
// ---------------------------------------------------------------------------
|
|
@@ -701,123 +285,111 @@ async function runDoctor({ cwd }) {
|
|
|
701
285
|
let allGood = true;
|
|
702
286
|
|
|
703
287
|
// 1. package.json + open-classify dep.
|
|
704
|
-
const
|
|
705
|
-
if (
|
|
288
|
+
const pkg = readJsonIfExists(join(cwd, "package.json"));
|
|
289
|
+
if (pkg === null) {
|
|
706
290
|
process.stdout.write("✖ No package.json — not a Node project\n");
|
|
707
291
|
allGood = false;
|
|
292
|
+
} else if (isOpenClassifyDep(pkg)) {
|
|
293
|
+
process.stdout.write("✓ open-classify found in package.json\n");
|
|
708
294
|
} else {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
if (isOpenClassifyDep(pkg)) {
|
|
712
|
-
process.stdout.write("✓ open-classify found in package.json\n");
|
|
713
|
-
} else {
|
|
714
|
-
process.stdout.write("⚠ open-classify not listed as a dependency\n");
|
|
715
|
-
allGood = false;
|
|
716
|
-
}
|
|
295
|
+
process.stdout.write("⚠ open-classify not listed as a dependency — run: npm install open-classify\n");
|
|
296
|
+
allGood = false;
|
|
717
297
|
}
|
|
718
298
|
|
|
719
|
-
// 2. Config parses.
|
|
720
|
-
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;
|
|
721
303
|
if (!existsSync(configPath)) {
|
|
722
|
-
process.stdout.write(
|
|
304
|
+
process.stdout.write(`✖ ./${PROJECT_DIRNAME}/config.json not found — run: npx open-classify init\n`);
|
|
723
305
|
allGood = false;
|
|
724
306
|
} else {
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
config
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
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);
|
|
733
315
|
if (existsSync(catalogPath)) {
|
|
734
|
-
process.stdout.write(`✓ ${
|
|
316
|
+
process.stdout.write(`✓ catalog found at ${relative(cwd, catalogPath)}\n`);
|
|
735
317
|
} else {
|
|
736
|
-
process.stdout.write(`✖
|
|
318
|
+
process.stdout.write(`✖ catalog not found at ${relative(cwd, catalogPath)}\n`);
|
|
737
319
|
allGood = false;
|
|
738
320
|
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
739
323
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
process.stdout.write(`✓ Model ${model} is available\n`);
|
|
753
|
-
} else {
|
|
754
|
-
process.stdout.write(`✖ Model ${model} not found — run: ollama pull ${model}\n`);
|
|
755
|
-
allGood = false;
|
|
756
|
-
}
|
|
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`);
|
|
757
336
|
} else {
|
|
758
|
-
process.stdout.write(`✖
|
|
337
|
+
process.stdout.write(`✖ Model ${defaultModel} not found — run: ollama pull ${defaultModel}\n`);
|
|
759
338
|
allGood = false;
|
|
760
339
|
}
|
|
761
|
-
}
|
|
762
|
-
process.stdout.write(`✖ Ollama
|
|
340
|
+
} else {
|
|
341
|
+
process.stdout.write(`✖ Ollama responded ${res.status} at ${host}\n`);
|
|
763
342
|
allGood = false;
|
|
764
343
|
}
|
|
765
344
|
} catch {
|
|
766
|
-
process.stdout.write(
|
|
345
|
+
process.stdout.write(`✖ Ollama not reachable at ${host} — is it running?\n`);
|
|
767
346
|
allGood = false;
|
|
768
347
|
}
|
|
769
348
|
}
|
|
770
349
|
|
|
771
|
-
//
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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`);
|
|
798
385
|
}
|
|
799
386
|
}
|
|
800
|
-
} catch { /* skip */ }
|
|
801
|
-
if (bad === 0) {
|
|
802
|
-
process.stdout.write(
|
|
803
|
-
active > 0
|
|
804
|
-
? `✓ ${active} active classifier(s) in ${classifiersRel}/\n`
|
|
805
|
-
: `ℹ No active classifiers in ${classifiersRel}/ (enable stock in config or customize a _name template)\n`,
|
|
806
|
-
);
|
|
807
387
|
}
|
|
808
388
|
}
|
|
809
389
|
|
|
810
390
|
if (!allGood) process.exit(1);
|
|
811
391
|
}
|
|
812
392
|
|
|
813
|
-
function configFromFile(cwd) {
|
|
814
|
-
try {
|
|
815
|
-
return JSON.parse(readFileSync(join(cwd, "open-classify.config.json"), "utf8"));
|
|
816
|
-
} catch {
|
|
817
|
-
return null;
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
393
|
// ---------------------------------------------------------------------------
|
|
822
394
|
// try
|
|
823
395
|
// ---------------------------------------------------------------------------
|
|
@@ -828,14 +400,12 @@ async function runTry({ cwd, message }) {
|
|
|
828
400
|
process.exit(1);
|
|
829
401
|
}
|
|
830
402
|
|
|
831
|
-
const configPath = join(cwd, "
|
|
403
|
+
const configPath = join(cwd, PROJECT_DIRNAME, "config.json");
|
|
832
404
|
if (!existsSync(configPath)) {
|
|
833
|
-
process.stderr.write(
|
|
405
|
+
process.stderr.write(`✖ ./${PROJECT_DIRNAME}/config.json not found — run: npx open-classify init\n`);
|
|
834
406
|
process.exit(1);
|
|
835
407
|
}
|
|
836
408
|
|
|
837
|
-
// Try loading from the consumer's node_modules first, then fall back to the
|
|
838
|
-
// package root (useful when running from the development checkout).
|
|
839
409
|
let createClassifier;
|
|
840
410
|
const candidates = [
|
|
841
411
|
join(cwd, "node_modules", "open-classify", "dist", "src", "index.js"),
|
|
@@ -844,10 +414,9 @@ async function runTry({ cwd, message }) {
|
|
|
844
414
|
for (const candidate of candidates) {
|
|
845
415
|
if (!existsSync(candidate)) continue;
|
|
846
416
|
try {
|
|
847
|
-
|
|
848
|
-
createClassifier = mod.createClassifier;
|
|
417
|
+
({ createClassifier } = await import(candidate));
|
|
849
418
|
break;
|
|
850
|
-
} catch {
|
|
419
|
+
} catch {/* try next */}
|
|
851
420
|
}
|
|
852
421
|
|
|
853
422
|
if (!createClassifier) {
|
|
@@ -858,17 +427,9 @@ async function runTry({ cwd, message }) {
|
|
|
858
427
|
process.exit(1);
|
|
859
428
|
}
|
|
860
429
|
|
|
861
|
-
const classifiersDir = join(cwd, "classifiers");
|
|
862
430
|
let classifier;
|
|
863
431
|
try {
|
|
864
|
-
|
|
865
|
-
const hasConfiguredClassifierDirs = Array.isArray(config?.classifiers?.dirs);
|
|
866
|
-
classifier = createClassifier({
|
|
867
|
-
configPath,
|
|
868
|
-
extraClassifierDirs:
|
|
869
|
-
hasConfiguredClassifierDirs || !existsSync(classifiersDir) ? [] : [classifiersDir],
|
|
870
|
-
skipResourceCheck: false,
|
|
871
|
-
});
|
|
432
|
+
classifier = createClassifier({ configPath });
|
|
872
433
|
} catch (err) {
|
|
873
434
|
process.stderr.write(`✖ Failed to initialise classifier: ${err.message}\n`);
|
|
874
435
|
process.exit(1);
|
|
@@ -886,30 +447,52 @@ async function runTry({ cwd, message }) {
|
|
|
886
447
|
}
|
|
887
448
|
|
|
888
449
|
// ---------------------------------------------------------------------------
|
|
889
|
-
//
|
|
450
|
+
// Shared helpers
|
|
890
451
|
// ---------------------------------------------------------------------------
|
|
891
452
|
|
|
892
|
-
function
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
}
|
|
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
|
+
}
|
|
901
462
|
}
|
|
902
463
|
|
|
903
|
-
function
|
|
904
|
-
|
|
905
|
-
|
|
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) => {
|
|
906
491
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
907
|
-
rl.question(
|
|
492
|
+
rl.question(prompt, (answer) => {
|
|
908
493
|
rl.close();
|
|
909
494
|
const v = (answer || "").trim().toLowerCase();
|
|
910
|
-
|
|
911
|
-
else if (v === "diff") resolve("diff");
|
|
912
|
-
else resolve("N");
|
|
495
|
+
resolveAnswer(defaultYes ? (v === "" || v === "y" || v === "yes") : (v === "y" || v === "yes"));
|
|
913
496
|
});
|
|
914
497
|
});
|
|
915
498
|
}
|