@okf-harness/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @okf-harness/cli
2
+
3
+ Command-line package for OKF Harness local workspaces. It provides the `okfh` command for initializing workspaces, registering sources, linting wiki content, searching and reading pages, generating graph reports, and installing Claude Code or Codex guidance.
4
+
5
+ OKF Harness is an independent open-source project built on [Andrej Karpathy's LLM Wiki](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f) pattern and Google's [Open Knowledge Format](https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing) / [OKF specification](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md).
6
+
7
+ Install:
8
+
9
+ ```bash
10
+ npm install -g @okf-harness/cli
11
+ okfh doctor --json
12
+ ```
13
+
14
+ Try without a global install:
15
+
16
+ ```bash
17
+ npx --package @okf-harness/cli okfh doctor --json
18
+ ```
19
+
20
+ For project overview, workflows, and security notes, see the [main repository README](https://github.com/pumblus/okf-harness#readme).
@@ -0,0 +1,1028 @@
1
+ // src/index.ts
2
+ import { execFile as execFile2 } from "child_process";
3
+ import path2 from "path";
4
+ import { installAgentAdapters } from "@okf-harness/agent-pack";
5
+ import {
6
+ addSource,
7
+ buildWorkspaceGraph,
8
+ createIngestPlan,
9
+ initWorkspace,
10
+ lintWorkspace,
11
+ listSources,
12
+ readWorkspaceDocument,
13
+ readWorkspaceStatus as readWorkspaceStatus2,
14
+ resolveWorkspaceRoot as resolveWorkspaceRoot2,
15
+ SourceManagementError,
16
+ searchWorkspace,
17
+ WorkspaceInitError as WorkspaceInitError2
18
+ } from "@okf-harness/core";
19
+ import { Command } from "commander";
20
+
21
+ // src/doctor/index.ts
22
+ import { execFile } from "child_process";
23
+ import { access, readFile } from "fs/promises";
24
+ import path from "path";
25
+ import { promisify } from "util";
26
+ import {
27
+ readWorkspaceStatus,
28
+ resolveWorkspaceRoot,
29
+ WorkspaceResolutionError
30
+ } from "@okf-harness/core";
31
+ var execFileAsync = promisify(execFile);
32
+ var requiredSkills = [
33
+ "okf-harness-init",
34
+ "okf-harness-ingest",
35
+ "okf-harness-query",
36
+ "okf-harness-maintain"
37
+ ];
38
+ async function runDoctor(options = {}) {
39
+ const checks = [
40
+ checkOkfh(),
41
+ checkNode(),
42
+ await checkExecutable("git", ["--version"], {
43
+ id: "git",
44
+ label: "git",
45
+ missingMessage: "git executable was not found."
46
+ }),
47
+ await checkExecutable("pnpm", ["--version"], {
48
+ id: "pnpm",
49
+ label: "pnpm",
50
+ missingMessage: "pnpm executable was not found.",
51
+ outputPrefix: "pnpm "
52
+ })
53
+ ];
54
+ const workspaceRoot = await resolveDoctorWorkspace(options, checks);
55
+ if (workspaceRoot === null) {
56
+ checks.push(skipCheck("workspace-status", "Workspace status", "No workspace was resolved."));
57
+ checks.push(skipCheck("claude-adapter", "Claude Code adapter", "No workspace was resolved."));
58
+ checks.push(skipCheck("codex-adapter", "Codex adapter", "No workspace was resolved."));
59
+ } else {
60
+ checks.push(await checkWorkspaceStatus(workspaceRoot));
61
+ checks.push(await checkAdapter(workspaceRoot, "claude"));
62
+ checks.push(await checkAdapter(workspaceRoot, "codex"));
63
+ }
64
+ const summary = summarizeChecks(checks);
65
+ return {
66
+ ok: summary.fail === 0,
67
+ workspace: workspaceRoot,
68
+ checks,
69
+ summary
70
+ };
71
+ }
72
+ function checkOkfh() {
73
+ return {
74
+ id: "okfh",
75
+ label: "okfh CLI",
76
+ status: "pass",
77
+ message: "The current okfh CLI entrypoint is running.",
78
+ details: {
79
+ argv0: process.argv[1] ?? null,
80
+ pid: process.pid
81
+ }
82
+ };
83
+ }
84
+ function checkNode() {
85
+ const version = process.versions.node;
86
+ const major = Number.parseInt(version.split(".")[0] ?? "", 10);
87
+ if (Number.isFinite(major) && major >= 22) {
88
+ return {
89
+ id: "node",
90
+ label: "Node.js",
91
+ status: "pass",
92
+ message: `Node.js ${version} satisfies the >=22 runtime requirement.`,
93
+ details: { version }
94
+ };
95
+ }
96
+ return {
97
+ id: "node",
98
+ label: "Node.js",
99
+ status: "fail",
100
+ message: `Node.js ${version} does not satisfy the >=22 runtime requirement.`,
101
+ details: { version, required: ">=22.0.0" }
102
+ };
103
+ }
104
+ async function checkExecutable(executable, args, options) {
105
+ try {
106
+ const { stdout, stderr } = await execFileAsync(executable, args);
107
+ const output = `${stdout}${stderr}`.trim();
108
+ return {
109
+ id: options.id,
110
+ label: options.label,
111
+ status: "pass",
112
+ message: output.length > 0 ? `${options.outputPrefix ?? ""}${output}` : `${executable} is available.`,
113
+ details: { executable }
114
+ };
115
+ } catch (error) {
116
+ const code = nodeErrorCode(error);
117
+ return {
118
+ id: options.id,
119
+ label: options.label,
120
+ status: "fail",
121
+ message: code === "ENOENT" ? options.missingMessage : `${executable} check failed.`,
122
+ details: {
123
+ executable,
124
+ error: error instanceof Error ? error.message : String(error)
125
+ }
126
+ };
127
+ }
128
+ }
129
+ async function resolveDoctorWorkspace(options, checks) {
130
+ try {
131
+ return await resolveWorkspaceRoot({
132
+ workspaceRoot: options.workspaceRoot,
133
+ startDir: options.startDir
134
+ });
135
+ } catch (error) {
136
+ if (error instanceof WorkspaceResolutionError) {
137
+ checks.push({
138
+ id: "workspace-resolution",
139
+ label: "Workspace resolution",
140
+ status: options.workspaceRoot === void 0 ? "warn" : "fail",
141
+ message: options.workspaceRoot === void 0 ? "No okfh.config.yaml was found from the current directory or its parents." : "The requested workspace could not be resolved.",
142
+ details: { startDir: error.startDir }
143
+ });
144
+ return null;
145
+ }
146
+ throw error;
147
+ }
148
+ }
149
+ async function checkWorkspaceStatus(workspaceRoot) {
150
+ const status = await readWorkspaceStatus(workspaceRoot);
151
+ if (!status.initialized) {
152
+ return {
153
+ id: "workspace-status",
154
+ label: "Workspace status",
155
+ status: "fail",
156
+ message: "Workspace is not initialized or okfh.config.yaml is invalid.",
157
+ details: {
158
+ workspace: status.workspaceRoot,
159
+ lintIssues: status.lint.issues.length
160
+ }
161
+ };
162
+ }
163
+ return {
164
+ id: "workspace-status",
165
+ label: "Workspace status",
166
+ status: status.lint.ok ? "pass" : "warn",
167
+ message: status.lint.ok ? `Workspace ${status.name ?? workspaceRoot} is initialized and lint passes.` : `Workspace ${status.name ?? workspaceRoot} is initialized but lint has issues.`,
168
+ details: {
169
+ workspace: status.workspaceRoot,
170
+ name: status.name ?? null,
171
+ wikiFiles: status.wikiFiles,
172
+ concepts: status.concepts,
173
+ lintOk: status.lint.ok,
174
+ lintIssues: status.lint.issues.length
175
+ }
176
+ };
177
+ }
178
+ async function checkAdapter(workspaceRoot, adapter) {
179
+ const rootGuidance = adapter === "claude" ? "CLAUDE.md" : "AGENTS.md";
180
+ const skillRoot = adapter === "claude" ? ".claude/skills" : ".agents/skills";
181
+ const missingFiles = [];
182
+ const rootPath = path.join(workspaceRoot, rootGuidance);
183
+ const rootContents = await readOptionalText(rootPath);
184
+ if (rootContents === void 0) {
185
+ missingFiles.push(rootGuidance);
186
+ }
187
+ for (const skill of requiredSkills) {
188
+ const skillPath = `${skillRoot}/${skill}/SKILL.md`;
189
+ if (!await fileExists(path.join(workspaceRoot, skillPath))) {
190
+ missingFiles.push(skillPath);
191
+ }
192
+ }
193
+ const hasManagedBlock = rootContents?.includes("<!-- OKF Harness: start -->") === true && rootContents.includes("<!-- OKF Harness: end -->");
194
+ if (missingFiles.length === 0 && hasManagedBlock) {
195
+ return {
196
+ id: `${adapter}-adapter`,
197
+ label: adapter === "claude" ? "Claude Code adapter" : "Codex adapter",
198
+ status: "pass",
199
+ message: `${adapter === "claude" ? "Claude Code" : "Codex"} adapter files are installed.`,
200
+ details: { rootGuidance, skillRoot }
201
+ };
202
+ }
203
+ return {
204
+ id: `${adapter}-adapter`,
205
+ label: adapter === "claude" ? "Claude Code adapter" : "Codex adapter",
206
+ status: "warn",
207
+ message: `${adapter === "claude" ? "Claude Code" : "Codex"} adapter support is incomplete.`,
208
+ details: {
209
+ rootGuidance,
210
+ skillRoot,
211
+ hasManagedBlock,
212
+ missingFiles,
213
+ repairCommand: `okfh agent install ${adapter} --workspace <workspace> --json`
214
+ }
215
+ };
216
+ }
217
+ function skipCheck(id, label, message) {
218
+ return {
219
+ id,
220
+ label,
221
+ status: "skip",
222
+ message
223
+ };
224
+ }
225
+ function summarizeChecks(checks) {
226
+ return checks.reduce(
227
+ (summary, check) => {
228
+ summary[check.status] += 1;
229
+ return summary;
230
+ },
231
+ { pass: 0, warn: 0, fail: 0, skip: 0 }
232
+ );
233
+ }
234
+ async function fileExists(filePath) {
235
+ try {
236
+ await access(filePath);
237
+ return true;
238
+ } catch (error) {
239
+ if (nodeErrorCode(error) === "ENOENT") {
240
+ return false;
241
+ }
242
+ throw error;
243
+ }
244
+ }
245
+ async function readOptionalText(filePath) {
246
+ try {
247
+ return await readFile(filePath, "utf8");
248
+ } catch (error) {
249
+ if (nodeErrorCode(error) === "ENOENT") {
250
+ return void 0;
251
+ }
252
+ throw error;
253
+ }
254
+ }
255
+ function nodeErrorCode(error) {
256
+ if (typeof error !== "object" || error === null || !("code" in error)) {
257
+ return void 0;
258
+ }
259
+ const code = error.code;
260
+ return typeof code === "string" ? code : void 0;
261
+ }
262
+
263
+ // src/errors/index.ts
264
+ import {
265
+ GraphWorkspaceError,
266
+ ReadWorkspaceError,
267
+ WorkspaceInitError,
268
+ WorkspaceResolutionError as WorkspaceResolutionError2
269
+ } from "@okf-harness/core";
270
+ function handleCliError(error, io, options) {
271
+ if (error instanceof WorkspaceInitError) {
272
+ writeCliError(io, {
273
+ command: "init",
274
+ error,
275
+ json: options.json
276
+ });
277
+ return 1;
278
+ }
279
+ if (error instanceof WorkspaceResolutionError2) {
280
+ writeCliError(io, {
281
+ command: options.command,
282
+ error,
283
+ workspace: null,
284
+ next: ["Run from inside an OKF Harness workspace or pass --workspace <path>."],
285
+ json: options.json
286
+ });
287
+ return 1;
288
+ }
289
+ if (isCommanderError(error)) {
290
+ if (options.json) {
291
+ writeCliError(io, {
292
+ command: options.command,
293
+ error: {
294
+ code: error.code,
295
+ message: error.message
296
+ },
297
+ json: true
298
+ });
299
+ } else {
300
+ io.writeErr(options.capturedStderr);
301
+ }
302
+ return error.exitCode;
303
+ }
304
+ writeCliError(io, {
305
+ command: "unknown",
306
+ error: {
307
+ code: "UNKNOWN",
308
+ message: error instanceof Error ? error.message : "Unknown error."
309
+ },
310
+ json: options.json
311
+ });
312
+ return 5;
313
+ }
314
+ function writeCliError(io, options) {
315
+ const normalized = normalizeCliError(options.error);
316
+ if (normalized === void 0) {
317
+ return false;
318
+ }
319
+ const envelope = {
320
+ ok: false,
321
+ command: options.command,
322
+ data: {},
323
+ warnings: [],
324
+ error: normalized,
325
+ next: options.next ?? []
326
+ };
327
+ if (options.workspace !== void 0) {
328
+ envelope.workspace = options.workspace;
329
+ }
330
+ if (options.json === true) {
331
+ io.writeErr(`${JSON.stringify(envelope)}
332
+ `);
333
+ } else {
334
+ io.writeErr(renderHumanError(normalized.message, options.next ?? []));
335
+ }
336
+ return true;
337
+ }
338
+ function writeValidationError(io, options) {
339
+ writeCliError(io, {
340
+ command: options.command,
341
+ error: options.details === void 0 ? {
342
+ code: options.code,
343
+ message: options.message
344
+ } : {
345
+ code: options.code,
346
+ message: options.message,
347
+ details: options.details
348
+ },
349
+ workspace: options.workspace,
350
+ next: options.next ?? [],
351
+ json: options.json
352
+ });
353
+ }
354
+ function renderHumanError(message, next) {
355
+ const nextStep = next[0];
356
+ return nextStep === void 0 ? `${message}
357
+ ` : `${message}
358
+ Next: ${nextStep}
359
+ `;
360
+ }
361
+ function normalizeCliError(error) {
362
+ if (isNormalizedCliError(error)) {
363
+ return normalizeObject(error);
364
+ }
365
+ if (error instanceof WorkspaceResolutionError2) {
366
+ return {
367
+ code: error.code,
368
+ message: error.message,
369
+ details: { startDir: error.startDir }
370
+ };
371
+ }
372
+ if (error instanceof ReadWorkspaceError) {
373
+ const normalized = {
374
+ code: error.code,
375
+ message: error.message
376
+ };
377
+ return Object.keys(error.details).length > 0 ? { ...normalized, details: error.details } : normalized;
378
+ }
379
+ if (error instanceof GraphWorkspaceError) {
380
+ return {
381
+ code: error.code,
382
+ message: error.message,
383
+ details: error.details
384
+ };
385
+ }
386
+ if (isErrorWithCode(error)) {
387
+ return {
388
+ code: error.code,
389
+ message: error.message
390
+ };
391
+ }
392
+ return void 0;
393
+ }
394
+ function normalizeObject(error) {
395
+ return error.details === void 0 ? {
396
+ code: error.code,
397
+ message: error.message
398
+ } : error;
399
+ }
400
+ function isCommanderError(error) {
401
+ if (typeof error !== "object" || error === null) {
402
+ return false;
403
+ }
404
+ const candidate = error;
405
+ return typeof candidate.code === "string" && typeof candidate.exitCode === "number" && typeof candidate.message === "string";
406
+ }
407
+ function isErrorWithCode(error) {
408
+ if (typeof error !== "object" || error === null) {
409
+ return false;
410
+ }
411
+ const candidate = error;
412
+ return typeof candidate.code === "string" && typeof candidate.message === "string";
413
+ }
414
+ function isNormalizedCliError(error) {
415
+ if (!isErrorWithCode(error)) {
416
+ return false;
417
+ }
418
+ const candidate = error;
419
+ return candidate.details === void 0 || isRecord(candidate.details);
420
+ }
421
+ function isRecord(value) {
422
+ return typeof value === "object" && value !== null && !Array.isArray(value);
423
+ }
424
+
425
+ // src/options/index.ts
426
+ function parseIntegerOption(value) {
427
+ const parsed = Number.parseInt(value, 10);
428
+ if (!Number.isFinite(parsed)) {
429
+ throw new Error(`Expected an integer option value, received: ${value}`);
430
+ }
431
+ return parsed;
432
+ }
433
+ function commandFromArgv(argv) {
434
+ const command = argv.slice(2).find((arg) => !arg.startsWith("-"));
435
+ return command ?? "unknown";
436
+ }
437
+ function parseAgentInstallTarget(input) {
438
+ if (input === "claude" || input === "codex" || input === "all") {
439
+ return input;
440
+ }
441
+ return void 0;
442
+ }
443
+ function parseInitAgentTarget(input) {
444
+ if (input === "none") {
445
+ return "none";
446
+ }
447
+ if (input === "claude,codex" || input === "codex,claude") {
448
+ return "all";
449
+ }
450
+ return parseAgentInstallTarget(input);
451
+ }
452
+
453
+ // src/render/result.ts
454
+ function writeResult(io, envelope, json = false) {
455
+ if (json) {
456
+ io.writeOut(`${JSON.stringify(envelope)}
457
+ `);
458
+ return;
459
+ }
460
+ io.writeOut(renderHumanResult(envelope));
461
+ }
462
+ function renderHumanResult(envelope) {
463
+ if (envelope.command === "search") {
464
+ const data = envelope.data;
465
+ const rows = (data.results ?? []).map((result, index) => {
466
+ const title = result.title ?? "(untitled)";
467
+ const pathValue = result.path ?? "(unknown path)";
468
+ const type = result.type ?? "Unknown";
469
+ const score = result.score === void 0 ? "" : ` score=${result.score}`;
470
+ return `${index + 1}. ${title} [${type}] ${pathValue}${score}`;
471
+ });
472
+ const summary = `Found ${data.totalMatches ?? rows.length}${data.truncated ? " (truncated)" : ""}`;
473
+ return `${summary}
474
+ ${rows.join("\n")}${rows.length > 0 ? "\n" : ""}`;
475
+ }
476
+ if (envelope.command === "read") {
477
+ const data = envelope.data;
478
+ const title = data.metadata?.title ?? "(untitled)";
479
+ const type = data.metadata?.type ?? "Unknown";
480
+ const pathValue = data.target?.path ?? "(unknown path)";
481
+ const truncated = data.content?.truncated ? " truncated" : "";
482
+ return `${title} [${type}] ${pathValue}${truncated}
483
+
484
+ ${data.content?.text ?? ""}
485
+ `;
486
+ }
487
+ if (envelope.command === "graph") {
488
+ const data = envelope.data;
489
+ return `Graph report: ${data.report?.htmlPath ?? "(not written)"}
490
+ Backlinks: ${data.report?.backlinksPath ?? "(not written)"}
491
+ `;
492
+ }
493
+ if (envelope.command === "doctor") {
494
+ const data = envelope.data;
495
+ const summary = data.summary ?? {};
496
+ const rows = (data.checks ?? []).map((check) => {
497
+ const label = check.label ?? "Check";
498
+ const status = (check.status ?? "unknown").toUpperCase();
499
+ const message = check.message ?? "";
500
+ return `${status} ${label}: ${message}`;
501
+ });
502
+ return `Doctor: ${summary.pass ?? 0} pass, ${summary.warn ?? 0} warn, ${summary.fail ?? 0} fail, ${summary.skip ?? 0} skip
503
+ ${rows.join("\n")}${rows.length > 0 ? "\n" : ""}`;
504
+ }
505
+ if (!envelope.ok) {
506
+ return `FAILED ${envelope.command}
507
+ `;
508
+ }
509
+ return `${envelope.ok ? "OK" : "FAILED"} ${envelope.command}
510
+ `;
511
+ }
512
+
513
+ // src/index.ts
514
+ var packageInfo = {
515
+ name: "@okf-harness/cli",
516
+ role: "cli"
517
+ };
518
+ async function runCli(argv = process.argv, io = {
519
+ writeOut: (chunk) => process.stdout.write(chunk),
520
+ writeErr: (chunk) => process.stderr.write(chunk)
521
+ }) {
522
+ const program = new Command();
523
+ let exitCode = 0;
524
+ const jsonRequested = argv.includes("--json");
525
+ const capturedCommanderErrors = [];
526
+ program.name("okfh").description("OKF Harness command line interface.");
527
+ program.exitOverride();
528
+ program.command("init <workspace>").description("Initialize an OKF Harness workspace.").storeOptionsAsProperties(false).requiredOption("--name <name>", "workspace display name").option("--agents <agents>", "agent adapters to install: claude, codex, all, none", "all").option("--dry-run", "return the planned writes without creating files").option("--git", "initialize a git repository without committing").option("--json", "write machine-readable JSON").action(async (workspace, command) => {
529
+ const options = command.opts();
530
+ const agentTarget = parseInitAgentTarget(options.agents);
531
+ if (agentTarget === void 0) {
532
+ writeValidationError(io, {
533
+ command: "init",
534
+ code: "INVALID_AGENT_TARGET",
535
+ message: "Agents must be one of: claude, codex, all, none, claude,codex.",
536
+ workspace: path2.resolve(workspace),
537
+ next: ["Rerun okfh init with --agents all, claude, codex, none, or claude,codex."],
538
+ json: options.json === true
539
+ });
540
+ exitCode = 1;
541
+ return;
542
+ }
543
+ let result;
544
+ try {
545
+ result = await initWorkspace({
546
+ workspaceRoot: workspace,
547
+ name: options.name,
548
+ dryRun: options.dryRun === true,
549
+ git: options.git === true
550
+ });
551
+ } catch (error) {
552
+ if (error instanceof WorkspaceInitError2) {
553
+ writeCliError(io, {
554
+ command: "init",
555
+ error,
556
+ workspace: path2.resolve(workspace),
557
+ next: error.code === "INIT_NOT_EMPTY" ? ["Choose an empty directory, or run okfh doctor --workspace <path> --json."] : ["Fix the initialization input and rerun okfh init --json."],
558
+ json: options.json === true
559
+ });
560
+ exitCode = error.code === "DEPENDENCY_MISSING" ? 4 : 1;
561
+ return;
562
+ }
563
+ throw error;
564
+ }
565
+ const agentInstall = agentTarget === "none" ? void 0 : await installAgentAdapters({
566
+ workspaceRoot: result.workspaceRoot,
567
+ adapter: agentTarget,
568
+ dryRun: result.dryRun
569
+ });
570
+ const ok = result.lint.ok && (agentInstall?.conflicts.length ?? 0) === 0;
571
+ const plannedFiles = uniqueStrings(
572
+ result.dryRun ? [...result.files, ...agentInstall?.plannedFiles ?? []] : []
573
+ );
574
+ const files = uniqueStrings(
575
+ result.dryRun ? result.files : [
576
+ ...result.files,
577
+ ...agentInstall?.writtenFiles ?? [],
578
+ ...agentInstall?.replacedFiles ?? []
579
+ ]
580
+ );
581
+ const envelope = {
582
+ ok,
583
+ command: "init",
584
+ workspace: result.workspaceRoot,
585
+ data: {
586
+ name: result.name,
587
+ dryRun: result.dryRun,
588
+ git: result.git,
589
+ agents: renderInitAgentData(agentTarget, agentInstall),
590
+ files,
591
+ plannedFiles,
592
+ directories: result.directories,
593
+ lint: result.lint
594
+ },
595
+ warnings: filterAgentPackPendingWarnings(result.warnings),
596
+ next: [
597
+ "Use the generated OKF Harness skills from Claude Code or Codex.",
598
+ "Run okfh lint --workspace <path> --json after editing wiki files."
599
+ ]
600
+ };
601
+ writeResult(io, envelope, options.json);
602
+ exitCode = ok ? 0 : 1;
603
+ });
604
+ program.command("status").description("Report OKF Harness workspace status.").storeOptionsAsProperties(false).option("--workspace <path>", "workspace path").option("--json", "write machine-readable JSON").action(async (command) => {
605
+ const options = command.opts();
606
+ const workspaceRoot = await resolveWorkspaceRoot2({ workspaceRoot: options.workspace });
607
+ const result = await readWorkspaceStatus2(workspaceRoot);
608
+ const envelope = {
609
+ ok: result.initialized && result.lint.ok,
610
+ command: "status",
611
+ workspace: result.workspaceRoot,
612
+ data: {
613
+ initialized: result.initialized,
614
+ name: result.name,
615
+ wikiFiles: result.wikiFiles,
616
+ concepts: result.concepts,
617
+ lint: result.lint,
618
+ capabilities: {
619
+ search: "available",
620
+ read: "available",
621
+ graph: "available",
622
+ queryCommand: "not_available"
623
+ }
624
+ },
625
+ warnings: filterAgentPackPendingWarnings(result.warnings),
626
+ next: result.initialized ? ["Use okfh search and okfh read to answer from the synthesized wiki."] : []
627
+ };
628
+ writeResult(io, envelope, options.json);
629
+ exitCode = envelope.ok ? 0 : 1;
630
+ });
631
+ program.command("lint").description("Lint an OKF Harness workspace.").storeOptionsAsProperties(false).option("--workspace <path>", "workspace path").option("--json", "write machine-readable JSON").action(async (command) => {
632
+ const options = command.opts();
633
+ const workspaceRoot = await resolveWorkspaceRoot2({ workspaceRoot: options.workspace });
634
+ const lint = await lintWorkspace(workspaceRoot);
635
+ const envelope = {
636
+ ok: lint.ok,
637
+ command: "lint",
638
+ workspace: workspaceRoot,
639
+ data: lint,
640
+ warnings: [],
641
+ next: lint.ok ? [] : ["Fix lint errors and rerun okfh lint --workspace <path> --json."]
642
+ };
643
+ writeResult(io, envelope, options.json);
644
+ exitCode = lint.ok ? 0 : 1;
645
+ });
646
+ program.command("search <query>").description("Search synthesized OKF wiki concept documents.").storeOptionsAsProperties(false).option("--workspace <path>", "workspace path").option("--limit <number>", "maximum results to return", parseIntegerOption).option("--json", "write machine-readable JSON").action(async (query, command) => {
647
+ const options = command.opts();
648
+ let workspaceRoot = null;
649
+ try {
650
+ workspaceRoot = await resolveWorkspaceRoot2({ workspaceRoot: options.workspace });
651
+ const result = await searchWorkspace({
652
+ workspaceRoot,
653
+ query,
654
+ limit: options.limit
655
+ });
656
+ const { workspaceRoot: _workspaceRoot, warnings, ...data } = result;
657
+ const envelope = {
658
+ ok: true,
659
+ command: "search",
660
+ workspace: result.workspaceRoot,
661
+ data,
662
+ warnings,
663
+ next: result.totalMatches === 0 ? [
664
+ "Run okfh read index --json to inspect the wiki map.",
665
+ "Try broader keywords, or ingest sources first if the material is only registered raw source."
666
+ ] : ["Run okfh read <concept-id> --json for the most relevant candidate."]
667
+ };
668
+ writeResult(io, envelope, options.json);
669
+ exitCode = 0;
670
+ } catch (error) {
671
+ const handled = writeCliError(io, {
672
+ command: "search",
673
+ error,
674
+ workspace: workspaceRoot,
675
+ next: ["Check the workspace path and rerun okfh search --json."],
676
+ json: options.json === true
677
+ });
678
+ if (handled) {
679
+ exitCode = 1;
680
+ return;
681
+ }
682
+ throw error;
683
+ }
684
+ });
685
+ program.command("read <target>").description("Read a bounded OKF wiki document.").storeOptionsAsProperties(false).option("--workspace <path>", "workspace path").option("--section <heading>", "read a section by heading").option("--section-id <id>", "read a section by stable section id").option("--offset <number>", "read from a character offset", parseIntegerOption).option("--limit <number>", "maximum characters for range reads", parseIntegerOption).option("--full", "explicitly request a full bounded read").option("--json", "write machine-readable JSON").action(async (target, command) => {
686
+ const options = command.opts();
687
+ let workspaceRoot = null;
688
+ try {
689
+ workspaceRoot = await resolveWorkspaceRoot2({ workspaceRoot: options.workspace });
690
+ const result = await readWorkspaceDocument({
691
+ workspaceRoot,
692
+ target,
693
+ section: options.section,
694
+ sectionId: options.sectionId,
695
+ offset: options.offset,
696
+ limit: options.limit,
697
+ full: options.full === true
698
+ });
699
+ const { workspaceRoot: _workspaceRoot, warnings, ...data } = result;
700
+ const envelope = {
701
+ ok: true,
702
+ command: "read",
703
+ workspace: result.workspaceRoot,
704
+ data,
705
+ warnings,
706
+ next: result.content.truncated ? ["Use --section, --section-id, --offset/--limit, or --full to continue reading."] : []
707
+ };
708
+ writeResult(io, envelope, options.json);
709
+ exitCode = 0;
710
+ } catch (error) {
711
+ const handled = writeCliError(io, {
712
+ command: "read",
713
+ error,
714
+ workspace: workspaceRoot,
715
+ next: ["Run okfh search with broader keywords, then read one returned concept path."],
716
+ json: options.json === true
717
+ });
718
+ if (handled) {
719
+ exitCode = 1;
720
+ return;
721
+ }
722
+ throw error;
723
+ }
724
+ });
725
+ program.command("graph").description("Generate OKF backlinks data and a self-contained graph report.").storeOptionsAsProperties(false).option("--workspace <path>", "workspace path").option("--open", "open the generated graph report in the default macOS browser").option("--json", "write machine-readable JSON").action(async (command) => {
726
+ const options = command.opts();
727
+ let workspaceRoot = null;
728
+ try {
729
+ workspaceRoot = await resolveWorkspaceRoot2({ workspaceRoot: options.workspace });
730
+ const result = await buildWorkspaceGraph({ workspaceRoot });
731
+ if (options.open === true) {
732
+ await openGraphReport(result.report.htmlPath);
733
+ }
734
+ const { workspaceRoot: _workspaceRoot, ...data } = result;
735
+ const envelope = {
736
+ ok: true,
737
+ command: "graph",
738
+ workspace: result.workspaceRoot,
739
+ data,
740
+ warnings: [],
741
+ next: options.open === true ? [] : ["Open the graph HTML report in a browser if needed."]
742
+ };
743
+ writeResult(io, envelope, options.json);
744
+ exitCode = 0;
745
+ } catch (error) {
746
+ const handled = writeCliError(io, {
747
+ command: "graph",
748
+ error,
749
+ workspace: workspaceRoot,
750
+ next: ["Check write permissions under .okfh and rerun okfh graph --json."],
751
+ json: options.json === true
752
+ });
753
+ if (handled) {
754
+ exitCode = 1;
755
+ return;
756
+ }
757
+ throw error;
758
+ }
759
+ });
760
+ program.command("doctor").description("Check okfh, local shell dependencies, and workspace readiness.").storeOptionsAsProperties(false).option("--workspace <path>", "workspace path").option("--json", "write machine-readable JSON").action(async (command) => {
761
+ const options = command.opts();
762
+ const result = await runDoctor({ workspaceRoot: options.workspace });
763
+ const envelope = {
764
+ ok: result.ok,
765
+ command: "doctor",
766
+ workspace: result.workspace,
767
+ data: {
768
+ checks: result.checks,
769
+ summary: result.summary
770
+ },
771
+ warnings: result.checks.filter((check) => check.status === "warn").map((check) => ({
772
+ code: check.id.toUpperCase().replaceAll("-", "_"),
773
+ message: check.message
774
+ })),
775
+ next: result.ok ? ["Use okfh --json commands through the local shell for OKF Harness workflows."] : ["Fix failed checks, then rerun okfh doctor --json."]
776
+ };
777
+ writeResult(io, envelope, options.json);
778
+ exitCode = result.ok ? 0 : 1;
779
+ });
780
+ program.command("source <action> [input]").description("Register and list OKF Harness raw sources.").storeOptionsAsProperties(false).requiredOption("--workspace <path>", "workspace path").option("--dry-run", "return the planned source registration without writing files").option("--json", "write machine-readable JSON").action(async (actionInput, input, command) => {
781
+ const options = command.opts();
782
+ if (actionInput === "list") {
783
+ try {
784
+ const result = await listSources({ workspaceRoot: options.workspace });
785
+ const envelope = {
786
+ ok: true,
787
+ command: "source list",
788
+ workspace: result.workspaceRoot,
789
+ data: { sources: result.sources },
790
+ warnings: [],
791
+ next: []
792
+ };
793
+ writeResult(io, envelope, options.json);
794
+ exitCode = 0;
795
+ } catch (error) {
796
+ if (error instanceof SourceManagementError) {
797
+ writeCliError(io, {
798
+ command: "source list",
799
+ error,
800
+ workspace: path2.resolve(options.workspace),
801
+ next: ["Check the workspace path and rerun okfh source list --json."],
802
+ json: options.json === true
803
+ });
804
+ exitCode = 1;
805
+ return;
806
+ }
807
+ throw error;
808
+ }
809
+ return;
810
+ }
811
+ if (actionInput !== "add") {
812
+ writeValidationError(io, {
813
+ command: "source",
814
+ code: "INVALID_SOURCE_ACTION",
815
+ message: "Source action must be one of: add, list.",
816
+ workspace: path2.resolve(options.workspace),
817
+ next: ["Use okfh source add <path-or-url> --workspace <path> --json."],
818
+ json: options.json === true
819
+ });
820
+ exitCode = 1;
821
+ return;
822
+ }
823
+ if (input === void 0) {
824
+ writeValidationError(io, {
825
+ command: "source add",
826
+ code: "SOURCE_INPUT_REQUIRED",
827
+ message: "source add requires a file path or URL.",
828
+ workspace: path2.resolve(options.workspace),
829
+ next: ["Pass a local file path or URL to okfh source add."],
830
+ json: options.json === true
831
+ });
832
+ exitCode = 2;
833
+ return;
834
+ }
835
+ try {
836
+ const result = await addSource({
837
+ workspaceRoot: options.workspace,
838
+ input,
839
+ dryRun: options.dryRun === true
840
+ });
841
+ const envelope = {
842
+ ok: true,
843
+ command: "source add",
844
+ workspace: result.workspaceRoot,
845
+ data: {
846
+ action: result.action,
847
+ dryRun: result.dryRun,
848
+ source: result.source
849
+ },
850
+ warnings: [],
851
+ next: [`Run okfh ingest plan ${result.source.id} --workspace <path> --json.`]
852
+ };
853
+ writeResult(io, envelope, options.json);
854
+ exitCode = 0;
855
+ } catch (error) {
856
+ if (error instanceof SourceManagementError) {
857
+ writeCliError(io, {
858
+ command: "source add",
859
+ error,
860
+ workspace: path2.resolve(options.workspace),
861
+ next: ["Check the source input and workspace path, then rerun okfh source add --json."],
862
+ json: options.json === true
863
+ });
864
+ exitCode = error.code === "SOURCE_INPUT_UNSUPPORTED" ? 1 : 5;
865
+ return;
866
+ }
867
+ throw error;
868
+ }
869
+ });
870
+ program.command("ingest <action> <source>").description("Plan source ingestion into the OKF wiki.").storeOptionsAsProperties(false).requiredOption("--workspace <path>", "workspace path").option("--json", "write machine-readable JSON").action(async (actionInput, sourceInput, command) => {
871
+ const options = command.opts();
872
+ if (actionInput !== "plan") {
873
+ writeValidationError(io, {
874
+ command: "ingest",
875
+ code: "INVALID_INGEST_ACTION",
876
+ message: "Ingest action must be: plan.",
877
+ workspace: path2.resolve(options.workspace),
878
+ next: ["Use okfh ingest plan <source-id-or-path> --workspace <path> --json."],
879
+ json: options.json === true
880
+ });
881
+ exitCode = 1;
882
+ return;
883
+ }
884
+ try {
885
+ const result = await createIngestPlan({
886
+ workspaceRoot: options.workspace,
887
+ source: sourceInput
888
+ });
889
+ const envelope = {
890
+ ok: true,
891
+ command: "ingest plan",
892
+ workspace: result.workspaceRoot,
893
+ data: {
894
+ source: result.source,
895
+ recommendedReferencePath: result.recommendedReferencePath,
896
+ candidateConcepts: result.candidateConcepts,
897
+ checklist: result.checklist
898
+ },
899
+ warnings: [],
900
+ next: ["Use the ingest plan as the Agent checklist before editing wiki files."]
901
+ };
902
+ writeResult(io, envelope, options.json);
903
+ exitCode = 0;
904
+ } catch (error) {
905
+ if (error instanceof SourceManagementError) {
906
+ writeCliError(io, {
907
+ command: "ingest plan",
908
+ error,
909
+ workspace: path2.resolve(options.workspace),
910
+ next: ["Register the source first with okfh source add, then rerun okfh ingest plan."],
911
+ json: options.json === true
912
+ });
913
+ exitCode = error.code === "SOURCE_NOT_REGISTERED" ? 1 : 5;
914
+ return;
915
+ }
916
+ throw error;
917
+ }
918
+ });
919
+ program.command("agent <action> <adapter>").description("Install or repair Claude Code and Codex adapter files.").storeOptionsAsProperties(false).requiredOption("--workspace <path>", "workspace path").option("--dry-run", "return the planned writes without creating files").option("--force", "replace conflicting same-name adapter files").option("--json", "write machine-readable JSON").action(async (actionInput, adapterInput, command) => {
920
+ const options = command.opts();
921
+ if (actionInput !== "install") {
922
+ writeValidationError(io, {
923
+ command: "agent",
924
+ code: "INVALID_AGENT_ACTION",
925
+ message: "Agent action must be: install.",
926
+ workspace: path2.resolve(options.workspace),
927
+ next: ["Use okfh agent install claude|codex|all --workspace <path> --json."],
928
+ json: options.json === true
929
+ });
930
+ exitCode = 1;
931
+ return;
932
+ }
933
+ const adapter = parseAgentInstallTarget(adapterInput);
934
+ if (adapter === void 0) {
935
+ writeValidationError(io, {
936
+ command: "agent install",
937
+ code: "INVALID_AGENT_ADAPTER",
938
+ message: "Adapter must be one of: claude, codex, all.",
939
+ workspace: path2.resolve(options.workspace),
940
+ next: ["Rerun with adapter claude, codex, or all."],
941
+ json: options.json === true
942
+ });
943
+ exitCode = 1;
944
+ return;
945
+ }
946
+ const workspaceStatus = await readWorkspaceStatus2(options.workspace);
947
+ if (!workspaceStatus.initialized) {
948
+ writeValidationError(io, {
949
+ command: "agent install",
950
+ code: "WORKSPACE_NOT_INITIALIZED",
951
+ message: "Workspace is not initialized. Run okfh init first.",
952
+ workspace: workspaceStatus.workspaceRoot,
953
+ next: ["Run okfh init <workspace> --name <name> --agents all --json first."],
954
+ json: options.json === true
955
+ });
956
+ exitCode = 1;
957
+ return;
958
+ }
959
+ const result = await installAgentAdapters({
960
+ workspaceRoot: workspaceStatus.workspaceRoot,
961
+ adapter,
962
+ dryRun: options.dryRun === true,
963
+ force: options.force === true
964
+ });
965
+ const ok = result.conflicts.length === 0;
966
+ const envelope = {
967
+ ok,
968
+ command: "agent install",
969
+ workspace: workspaceStatus.workspaceRoot,
970
+ data: result,
971
+ warnings: [],
972
+ next: ok ? ["Run okfh status --workspace <path> --json to verify the workspace."] : ["Resolve conflicts or rerun with --force after reviewing the files."]
973
+ };
974
+ writeResult(io, envelope, options.json);
975
+ exitCode = ok ? 0 : 1;
976
+ });
977
+ const restoreConsoleError = captureCommanderConsoleError(capturedCommanderErrors);
978
+ try {
979
+ await program.parseAsync(argv);
980
+ return exitCode;
981
+ } catch (error) {
982
+ return handleCliError(error, io, {
983
+ command: commandFromArgv(argv),
984
+ json: jsonRequested,
985
+ capturedStderr: capturedCommanderErrors.join("")
986
+ });
987
+ } finally {
988
+ restoreConsoleError();
989
+ }
990
+ }
991
+ async function openGraphReport(htmlPath) {
992
+ await new Promise((resolve, reject) => {
993
+ execFile2("open", [htmlPath], (error) => {
994
+ if (error !== null) {
995
+ reject(error);
996
+ return;
997
+ }
998
+ resolve();
999
+ });
1000
+ });
1001
+ }
1002
+ function captureCommanderConsoleError(capturedErrors) {
1003
+ const originalConsoleError = console.error;
1004
+ console.error = (...args) => {
1005
+ capturedErrors.push(`${args.map((arg) => String(arg)).join(" ")}
1006
+ `);
1007
+ };
1008
+ return () => {
1009
+ console.error = originalConsoleError;
1010
+ };
1011
+ }
1012
+ function renderInitAgentData(requested, install) {
1013
+ if (install === void 0) {
1014
+ return { requested };
1015
+ }
1016
+ return { requested, install };
1017
+ }
1018
+ function filterAgentPackPendingWarnings(warnings) {
1019
+ return warnings.filter((warning) => warning.code !== "AGENT_PACK_PENDING");
1020
+ }
1021
+ function uniqueStrings(values) {
1022
+ return [...new Set(values)];
1023
+ }
1024
+
1025
+ export {
1026
+ packageInfo,
1027
+ runCli
1028
+ };
@@ -0,0 +1,29 @@
1
+ type CliIo = {
2
+ writeOut: (chunk: string) => void;
3
+ writeErr: (chunk: string) => void;
4
+ };
5
+ type JsonEnvelope = {
6
+ ok: boolean;
7
+ command: string;
8
+ workspace?: string | null;
9
+ data: unknown;
10
+ warnings: Array<{
11
+ code: string;
12
+ message: string;
13
+ }>;
14
+ next: string[];
15
+ error?: {
16
+ code: string;
17
+ message: string;
18
+ details?: Record<string, unknown>;
19
+ };
20
+ };
21
+
22
+ declare const packageInfo: {
23
+ readonly name: "@okf-harness/cli";
24
+ readonly role: "cli";
25
+ };
26
+ type PackageInfo = typeof packageInfo;
27
+ declare function runCli(argv?: string[], io?: CliIo): Promise<number>;
28
+
29
+ export { type CliIo, type JsonEnvelope, type PackageInfo, packageInfo, runCli };
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import {
2
+ packageInfo,
3
+ runCli
4
+ } from "./chunk-CU2XPEBG.js";
5
+ export {
6
+ packageInfo,
7
+ runCli
8
+ };
package/dist/main.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/main.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runCli
4
+ } from "./chunk-CU2XPEBG.js";
5
+
6
+ // src/main.ts
7
+ process.exitCode = await runCli(process.argv);
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@okf-harness/cli",
3
+ "version": "0.1.0",
4
+ "description": "The okfh command-line package for local OKF Harness workspaces.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "author": "Eric Zhou",
8
+ "engines": {
9
+ "node": ">=22.0.0"
10
+ },
11
+ "homepage": "https://github.com/pumblus/okf-harness#readme",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/pumblus/okf-harness.git",
15
+ "directory": "packages/cli"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/pumblus/okf-harness/issues"
19
+ },
20
+ "keywords": [
21
+ "okf",
22
+ "open-knowledge-format",
23
+ "llm-wiki",
24
+ "agent-skills",
25
+ "claude-code",
26
+ "codex",
27
+ "macos",
28
+ "knowledge-management"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "bin": {
36
+ "okfh": "dist/main.js",
37
+ "okf-harness": "dist/main.js"
38
+ },
39
+ "exports": {
40
+ ".": {
41
+ "types": "./dist/index.d.ts",
42
+ "import": "./dist/index.js"
43
+ }
44
+ },
45
+ "files": [
46
+ "dist"
47
+ ],
48
+ "scripts": {
49
+ "build": "pnpm --dir ../.. --filter @okf-harness/core build && pnpm --dir ../.. --filter @okf-harness/agent-pack build && tsup src/index.ts src/main.ts --format esm --dts --clean",
50
+ "prepublishOnly": "pnpm run build"
51
+ },
52
+ "dependencies": {
53
+ "@okf-harness/agent-pack": "0.1.0",
54
+ "@okf-harness/core": "0.1.0",
55
+ "commander": "4.1.1"
56
+ }
57
+ }