hafez 1.0.1
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/LICENSE +21 -0
- package/README.md +190 -0
- package/dist/cli/batch-errors.d.ts +24 -0
- package/dist/cli/batch-errors.js +236 -0
- package/dist/cli/commands-schema.d.ts +3 -0
- package/dist/cli/commands-schema.js +80 -0
- package/dist/cli/commands.d.ts +133 -0
- package/dist/cli/commands.js +628 -0
- package/dist/cli/format.d.ts +9 -0
- package/dist/cli/format.js +158 -0
- package/dist/cli/help-agent.d.ts +6 -0
- package/dist/cli/help-agent.js +158 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +304 -0
- package/dist/cli/resolve-vault.d.ts +13 -0
- package/dist/cli/resolve-vault.js +124 -0
- package/dist/cli/schema-introspect.d.ts +70 -0
- package/dist/cli/schema-introspect.js +372 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3 -0
- package/dist/contracts.d.ts +26 -0
- package/dist/contracts.js +87 -0
- package/dist/db.d.ts +68 -0
- package/dist/db.js +663 -0
- package/dist/digest.d.ts +29 -0
- package/dist/digest.js +94 -0
- package/dist/export-okf.d.ts +12 -0
- package/dist/export-okf.js +176 -0
- package/dist/git.d.ts +12 -0
- package/dist/git.js +355 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +909 -0
- package/dist/knowledge-index.d.ts +3 -0
- package/dist/knowledge-index.js +135 -0
- package/dist/merge.d.ts +1 -0
- package/dist/merge.js +176 -0
- package/dist/migrate-knowledge-v2.d.ts +11 -0
- package/dist/migrate-knowledge-v2.js +158 -0
- package/dist/migrate-next-actions.d.ts +6 -0
- package/dist/migrate-next-actions.js +32 -0
- package/dist/migrate-types.d.ts +11 -0
- package/dist/migrate-types.js +142 -0
- package/dist/parser.d.ts +18 -0
- package/dist/parser.js +68 -0
- package/dist/query.d.ts +24 -0
- package/dist/query.js +167 -0
- package/dist/schema.d.ts +9 -0
- package/dist/schema.js +59 -0
- package/dist/sections.d.ts +54 -0
- package/dist/sections.js +297 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +39 -0
- package/dist/types.d.ts +321 -0
- package/dist/types.js +11 -0
- package/dist/vault.d.ts +12 -0
- package/dist/vault.js +45 -0
- package/package.json +67 -0
- package/skill/SKILL.md +190 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// src/cli/resolve-vault.ts
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'fs';
|
|
3
|
+
import { join, resolve } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
export function getConfigDir() {
|
|
7
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
8
|
+
return join(xdg || join(homedir(), '.config'), 'hafez');
|
|
9
|
+
}
|
|
10
|
+
export function getConfigPath() {
|
|
11
|
+
return join(getConfigDir(), 'vault');
|
|
12
|
+
}
|
|
13
|
+
export function readVaultConfig() {
|
|
14
|
+
const configPath = getConfigPath();
|
|
15
|
+
if (!existsSync(configPath))
|
|
16
|
+
return null;
|
|
17
|
+
try {
|
|
18
|
+
const content = readFileSync(configPath, 'utf-8').trim();
|
|
19
|
+
return content || null;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function saveVaultConfig(vaultPath) {
|
|
26
|
+
const dir = getConfigDir();
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
writeFileSync(join(dir, 'vault'), resolve(vaultPath) + '\n');
|
|
29
|
+
}
|
|
30
|
+
function isVault(dirPath) {
|
|
31
|
+
return existsSync(dirPath) && statSync(dirPath).isDirectory() && existsSync(join(dirPath, 'entities'));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve vault path. Returns string or null.
|
|
35
|
+
* If config exists but points to a bad path, returns an error object instead.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveVaultPath(flagValue) {
|
|
38
|
+
// 1. Explicit flag
|
|
39
|
+
if (flagValue)
|
|
40
|
+
return flagValue;
|
|
41
|
+
// 2. Config file
|
|
42
|
+
const configValue = readVaultConfig();
|
|
43
|
+
if (configValue) {
|
|
44
|
+
if (isVault(configValue))
|
|
45
|
+
return configValue;
|
|
46
|
+
// Config exists but points to bad path — loud failure
|
|
47
|
+
return {
|
|
48
|
+
error: `Config (${getConfigPath()}) points to ${configValue} but it doesn't look like a vault.\nRun 'hafez init --register /correct/path' to fix.`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
export function noVaultMessage() {
|
|
54
|
+
const configPath = getConfigPath();
|
|
55
|
+
const configStatus = existsSync(configPath) ? 'path invalid' : 'no config file';
|
|
56
|
+
return `No vault found.
|
|
57
|
+
|
|
58
|
+
Checked:
|
|
59
|
+
--vault flag not provided
|
|
60
|
+
${configPath} ${configStatus}
|
|
61
|
+
|
|
62
|
+
Get started:
|
|
63
|
+
hafez init --register /path/to/vault Register an existing vault
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
export function cmdInit(args, flagValue) {
|
|
67
|
+
const registerIdx = args.indexOf('--register');
|
|
68
|
+
if (registerIdx !== -1) {
|
|
69
|
+
// hafez init --register <path>
|
|
70
|
+
const path = args[registerIdx + 1];
|
|
71
|
+
if (!path) {
|
|
72
|
+
process.stderr.write('Usage: hafez init --register <path>\n');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
const abs = resolve(path);
|
|
76
|
+
if (!isVault(abs)) {
|
|
77
|
+
process.stderr.write(`Error: ${abs} does not exist or is not a vault (missing entities/ directory).\n`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
// Every vault write is a git commit — catch the two states that would
|
|
81
|
+
// otherwise make every later command fail with a raw git error.
|
|
82
|
+
if (!existsSync(join(abs, '.git'))) {
|
|
83
|
+
process.stderr.write(`Error: ${abs} is not a git repository — hafez uses git for history and sync.\n` +
|
|
84
|
+
`Set it up, then re-register:\n` +
|
|
85
|
+
` cd ${abs} && git init && git commit --allow-empty -m "init vault"\n`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
let identityOk = false;
|
|
89
|
+
try {
|
|
90
|
+
const email = execSync('git config user.email', { cwd: abs, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
91
|
+
const name = execSync('git config user.name', { cwd: abs, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
92
|
+
identityOk = email.length > 0 && name.length > 0;
|
|
93
|
+
}
|
|
94
|
+
catch { /* unset config exits non-zero */ }
|
|
95
|
+
if (!identityOk) {
|
|
96
|
+
process.stderr.write(`Error: git has no identity configured — every vault write is a git commit.\n` +
|
|
97
|
+
`Fix (one-time), then re-register:\n` +
|
|
98
|
+
` git config --global user.name "Your Name"\n` +
|
|
99
|
+
` git config --global user.email "you@example.com"\n`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
saveVaultConfig(abs);
|
|
103
|
+
process.stdout.write(`Registered vault: ${abs}\n`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Bare `hafez init` — show current resolution status
|
|
107
|
+
if (flagValue) {
|
|
108
|
+
process.stdout.write(`Vault: ${flagValue} (--vault flag)\n`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const configValue = readVaultConfig();
|
|
112
|
+
if (configValue) {
|
|
113
|
+
if (isVault(configValue)) {
|
|
114
|
+
process.stdout.write(`Vault: ${configValue} (config: ${getConfigPath()})\n`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
process.stderr.write(`Config (${getConfigPath()}) points to ${configValue} but it doesn't look like a vault.\nRun 'hafez init --register /correct/path' to fix.\n`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
process.stderr.write(noVaultMessage());
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { BatchOperationSchema, SessionLogSchema, UpdateFieldsSchema } from './commands.js';
|
|
2
|
+
export interface FieldSchema {
|
|
3
|
+
type: string;
|
|
4
|
+
optional?: boolean;
|
|
5
|
+
nullable?: boolean;
|
|
6
|
+
enum?: readonly string[];
|
|
7
|
+
literal?: unknown;
|
|
8
|
+
items?: FieldSchema;
|
|
9
|
+
fields?: Record<string, FieldSchema>;
|
|
10
|
+
description?: string;
|
|
11
|
+
strict?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface OpSchema {
|
|
14
|
+
op: string;
|
|
15
|
+
/** For create: kind name (entity | knowledge | session). Undefined for others. */
|
|
16
|
+
kind?: string;
|
|
17
|
+
/** Top-level required/optional fields for the op body. */
|
|
18
|
+
fields: Record<string, FieldSchema>;
|
|
19
|
+
strict: boolean;
|
|
20
|
+
description?: string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* List every op in canonical order. `create` is one entry per kind — so the
|
|
24
|
+
* list has 8 entries, not 6. Each entry carries the display name the help
|
|
25
|
+
* renderer uses in the op catalog.
|
|
26
|
+
*/
|
|
27
|
+
export interface OpCatalogEntry {
|
|
28
|
+
op: string;
|
|
29
|
+
kind?: string;
|
|
30
|
+
/** Stable identifier used by `schema <name>`. `create-entity` / `update` / `link`. */
|
|
31
|
+
name: string;
|
|
32
|
+
description: string;
|
|
33
|
+
}
|
|
34
|
+
export declare const DIGEST_DESCRIPTION = "Convert session-context JSON (stdin) into a batch payload. Not a batch op \u2014 pipe: echo '<json>' | hafez digest | hafez batch";
|
|
35
|
+
/** Canonical digest input example. The drift test asserts it parses against
|
|
36
|
+
* DigestInputSchema, so it cannot silently diverge from the schema. */
|
|
37
|
+
export declare const DIGEST_EXAMPLE: Record<string, unknown>;
|
|
38
|
+
export declare function listOps(): OpCatalogEntry[];
|
|
39
|
+
/** Return the full introspected schema for a single op, by `name`. */
|
|
40
|
+
export declare function getOpSchema(name: string): OpSchema | null;
|
|
41
|
+
/**
|
|
42
|
+
* Look up an enum's values by dotted path. Supported paths include:
|
|
43
|
+
* - 'update.fields.status'
|
|
44
|
+
* - 'update.fields.confidence'
|
|
45
|
+
* - 'update.fields.session_log.type'
|
|
46
|
+
* - 'create-entity.fields.type'
|
|
47
|
+
* - 'create-knowledge.fields.subtype'
|
|
48
|
+
* - 'link.relation'
|
|
49
|
+
*
|
|
50
|
+
* Returns `null` if the path does not resolve to an enum field.
|
|
51
|
+
*/
|
|
52
|
+
export declare function getEnumValues(path: string): readonly string[] | null;
|
|
53
|
+
/**
|
|
54
|
+
* Build a minimal, valid-looking example payload for an op. Uses the first
|
|
55
|
+
* enum value for enums, "<slug>"/"<name>"/"text" placeholders for strings,
|
|
56
|
+
* 0 for numbers, empty arrays for arrays, and recurses for nested objects.
|
|
57
|
+
* Optional fields are skipped unless they are the only way to make the
|
|
58
|
+
* example illustrative (op / kind literals are always emitted).
|
|
59
|
+
*
|
|
60
|
+
* Aliased keys (per contracts.ts ALIAS_KEYS) are stripped from the output
|
|
61
|
+
* so only canonical names appear in rendered help.
|
|
62
|
+
*/
|
|
63
|
+
export declare function getOpExample(name: string): Record<string, unknown> | null;
|
|
64
|
+
/**
|
|
65
|
+
* Flatten an OpSchema to a JSON-schema-shaped plain object for the
|
|
66
|
+
* `hafez schema <op>` command. Not full draft-7 — just a readable
|
|
67
|
+
* structure agents can parse.
|
|
68
|
+
*/
|
|
69
|
+
export declare function zodToJsonSchema(name: string): Record<string, unknown> | null;
|
|
70
|
+
export { BatchOperationSchema, SessionLogSchema, UpdateFieldsSchema };
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// src/cli/schema-introspect.ts
|
|
2
|
+
//
|
|
3
|
+
// Single introspection surface that `help --agent`, `schema <op>`, and error
|
|
4
|
+
// formatters all read from. Walks the Zod v4 `BatchOperationSchema` tree to
|
|
5
|
+
// produce plain objects describing ops, fields, enums, and example payloads.
|
|
6
|
+
// The digest stdin pipe is introspected too (special-cased — it is not a
|
|
7
|
+
// batch op and stays out of listOps()).
|
|
8
|
+
//
|
|
9
|
+
// Hand-rolled walker (no zod-to-json-schema dependency). Supports the node
|
|
10
|
+
// types actually used in BatchOperationSchema plus a few forward-looking ones
|
|
11
|
+
// (`default`, `effects`, `union`) so the walker does not explode when the
|
|
12
|
+
// schema is extended.
|
|
13
|
+
import { BatchOperationSchema, SessionLogSchema, UpdateFieldsSchema, } from './commands.js';
|
|
14
|
+
import { DigestInputSchema } from '../digest.js';
|
|
15
|
+
import { ALIAS_KEYS } from '../contracts.js';
|
|
16
|
+
// --- Walker ---
|
|
17
|
+
/** Unwrap optional/nullable/default wrappers and mark the metadata on the result. */
|
|
18
|
+
function walk(node) {
|
|
19
|
+
if (!node)
|
|
20
|
+
return { type: 'unknown' };
|
|
21
|
+
const def = node._def;
|
|
22
|
+
if (!def)
|
|
23
|
+
return { type: 'unknown' };
|
|
24
|
+
const type = def.type;
|
|
25
|
+
// Zod v4 stores .describe() text on the schema's registry-backed
|
|
26
|
+
// `description` getter, not on _def — read both, _def first for
|
|
27
|
+
// forward-compatibility.
|
|
28
|
+
const description = def.description ?? node.description;
|
|
29
|
+
// Wrapper types — unwrap and annotate. A .describe() applied after the
|
|
30
|
+
// wrapper lands on the wrapper node, so carry it down to the inner schema.
|
|
31
|
+
if (type === 'optional') {
|
|
32
|
+
const inner = walk(def.innerType);
|
|
33
|
+
inner.optional = true;
|
|
34
|
+
inner.description ??= description;
|
|
35
|
+
return inner;
|
|
36
|
+
}
|
|
37
|
+
if (type === 'nullable') {
|
|
38
|
+
const inner = walk(def.innerType);
|
|
39
|
+
inner.nullable = true;
|
|
40
|
+
inner.description ??= description;
|
|
41
|
+
return inner;
|
|
42
|
+
}
|
|
43
|
+
if (type === 'default') {
|
|
44
|
+
const inner = walk(def.innerType);
|
|
45
|
+
inner.optional = true;
|
|
46
|
+
inner.description ??= description;
|
|
47
|
+
return inner;
|
|
48
|
+
}
|
|
49
|
+
if (type === 'pipe' || type === 'effects' || type === 'transform' || type === 'readonly') {
|
|
50
|
+
// Zod effects / transforms / pipes — descend to the inner schema.
|
|
51
|
+
const inner = def.in ?? def.schema ?? def.innerType;
|
|
52
|
+
if (inner)
|
|
53
|
+
return walk(inner);
|
|
54
|
+
}
|
|
55
|
+
// Leaf / container types.
|
|
56
|
+
if (type === 'string')
|
|
57
|
+
return { type: 'string', description };
|
|
58
|
+
if (type === 'number')
|
|
59
|
+
return { type: 'number', description };
|
|
60
|
+
if (type === 'boolean')
|
|
61
|
+
return { type: 'boolean', description };
|
|
62
|
+
if (type === 'null')
|
|
63
|
+
return { type: 'null' };
|
|
64
|
+
if (type === 'any' || type === 'unknown')
|
|
65
|
+
return { type: 'unknown' };
|
|
66
|
+
if (type === 'literal') {
|
|
67
|
+
const values = def.values;
|
|
68
|
+
const literal = Array.isArray(values) ? values[0] : def.value;
|
|
69
|
+
return { type: 'literal', literal };
|
|
70
|
+
}
|
|
71
|
+
if (type === 'enum') {
|
|
72
|
+
const entries = def.entries;
|
|
73
|
+
const values = entries && typeof entries === 'object'
|
|
74
|
+
? Object.values(entries)
|
|
75
|
+
: (Array.isArray(def.values) ? def.values : []);
|
|
76
|
+
return { type: 'enum', enum: values, description };
|
|
77
|
+
}
|
|
78
|
+
if (type === 'array') {
|
|
79
|
+
const element = def.element ?? def.type;
|
|
80
|
+
return { type: 'array', items: walk(element), description };
|
|
81
|
+
}
|
|
82
|
+
if (type === 'object') {
|
|
83
|
+
const rawShape = def.shape;
|
|
84
|
+
const shape = typeof rawShape === 'function' ? rawShape() : rawShape ?? {};
|
|
85
|
+
const fields = {};
|
|
86
|
+
for (const [k, v] of Object.entries(shape))
|
|
87
|
+
fields[k] = walk(v);
|
|
88
|
+
// Zod v4: strict objects use a catchall that throws on extra keys. For our
|
|
89
|
+
// purposes we flag every object we emit as strict (we opt in explicitly on
|
|
90
|
+
// all BatchOperationSchema objects) — the plan's error surface depends on it.
|
|
91
|
+
return { type: 'object', fields, strict: true, description };
|
|
92
|
+
}
|
|
93
|
+
if (type === 'union') {
|
|
94
|
+
// A plain z.union is uncommon in the current schema but possible. Return
|
|
95
|
+
// the first option as a representative. The error formatter has specific
|
|
96
|
+
// handling for union discriminators; introspection just needs shape.
|
|
97
|
+
const first = def.options?.[0];
|
|
98
|
+
if (first)
|
|
99
|
+
return walk(first);
|
|
100
|
+
return { type: 'unknown' };
|
|
101
|
+
}
|
|
102
|
+
return { type: 'unknown' };
|
|
103
|
+
}
|
|
104
|
+
// --- Branch discovery for the top-level schema ---
|
|
105
|
+
function topOptions() {
|
|
106
|
+
return BatchOperationSchema._def.options ?? [];
|
|
107
|
+
}
|
|
108
|
+
function shapeOf(obj) {
|
|
109
|
+
const rawShape = obj?._def?.shape;
|
|
110
|
+
return typeof rawShape === 'function' ? rawShape() : rawShape ?? {};
|
|
111
|
+
}
|
|
112
|
+
function literalOf(schema) {
|
|
113
|
+
const vs = schema?._def?.values;
|
|
114
|
+
return Array.isArray(vs) ? vs[0] : schema?._def?.value;
|
|
115
|
+
}
|
|
116
|
+
function unwrapOptional(s) {
|
|
117
|
+
let cur = s;
|
|
118
|
+
while (cur?._def?.type === 'optional' || cur?._def?.type === 'nullable')
|
|
119
|
+
cur = cur._def.innerType;
|
|
120
|
+
return cur;
|
|
121
|
+
}
|
|
122
|
+
function enumerateBranches() {
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const opt of topOptions()) {
|
|
125
|
+
if (opt?._def?.type === 'object') {
|
|
126
|
+
const shape = shapeOf(opt);
|
|
127
|
+
const op = literalOf(shape.op);
|
|
128
|
+
out.push({ op, branch: opt });
|
|
129
|
+
}
|
|
130
|
+
else if (opt?._def?.type === 'union') {
|
|
131
|
+
for (const inner of opt._def.options ?? []) {
|
|
132
|
+
const shape = shapeOf(inner);
|
|
133
|
+
const op = literalOf(shape.op);
|
|
134
|
+
const kind = literalOf(shape.kind);
|
|
135
|
+
out.push({ op, kind, branch: inner });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
// --- Digest (stdin pipe, NOT a batch op) ---
|
|
142
|
+
//
|
|
143
|
+
// `hafez digest` reads its own JSON from stdin and prints a batch payload:
|
|
144
|
+
// echo '<digest-json>' | hafez digest | hafez batch
|
|
145
|
+
// It is deliberately excluded from listOps() so the op catalog and batch
|
|
146
|
+
// examples never imply {"op":"digest"} is valid batch JSON. It still gets
|
|
147
|
+
// `hafez schema digest` via the special cases below.
|
|
148
|
+
export const DIGEST_DESCRIPTION = 'Convert session-context JSON (stdin) into a batch payload. Not a batch op — pipe: echo \'<json>\' | hafez digest | hafez batch';
|
|
149
|
+
/** Canonical digest input example. The drift test asserts it parses against
|
|
150
|
+
* DigestInputSchema, so it cannot silently diverge from the schema. */
|
|
151
|
+
export const DIGEST_EXAMPLE = {
|
|
152
|
+
entities_touched: ['slug-1', 'slug-2'],
|
|
153
|
+
decisions: ['Decision made'],
|
|
154
|
+
narrative: 'One to three sentence cross-cutting summary.',
|
|
155
|
+
session_date: '2026-01-15',
|
|
156
|
+
agent: 'claude',
|
|
157
|
+
};
|
|
158
|
+
const OP_DESCRIPTIONS = {
|
|
159
|
+
'update': 'Update an entity or knowledge note (status, brief, next actions, session log, synthesis, …).',
|
|
160
|
+
'create-entity': 'Create a new entity (capture / entity / project).',
|
|
161
|
+
'create-knowledge': 'Create a new knowledge note (insight / plan subtype).',
|
|
162
|
+
'create-session': 'Create a new session note linked to an entity.',
|
|
163
|
+
'capture': 'Quick capture — a raw inbox item with no structure required.',
|
|
164
|
+
'link': 'Add a parent or related link between two entities.',
|
|
165
|
+
'unlink': 'Remove a parent or related link between two entities.',
|
|
166
|
+
'promote': 'Promote a capture to entity/project/knowledge, or an entity to project.',
|
|
167
|
+
};
|
|
168
|
+
export function listOps() {
|
|
169
|
+
return enumerateBranches().map(({ op, kind }) => {
|
|
170
|
+
const name = kind ? `${op}-${kind}` : op;
|
|
171
|
+
return { op, kind, name, description: OP_DESCRIPTIONS[name] ?? '' };
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/** Return the full introspected schema for a single op, by `name`. */
|
|
175
|
+
export function getOpSchema(name) {
|
|
176
|
+
if (name === 'digest') {
|
|
177
|
+
const root = walk(DigestInputSchema);
|
|
178
|
+
return {
|
|
179
|
+
op: 'digest',
|
|
180
|
+
fields: root.fields ?? {},
|
|
181
|
+
strict: false,
|
|
182
|
+
description: DIGEST_DESCRIPTION,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
for (const { op, kind, branch } of enumerateBranches()) {
|
|
186
|
+
const n = kind ? `${op}-${kind}` : op;
|
|
187
|
+
if (n !== name)
|
|
188
|
+
continue;
|
|
189
|
+
const shape = shapeOf(branch);
|
|
190
|
+
const fields = {};
|
|
191
|
+
for (const [k, v] of Object.entries(shape))
|
|
192
|
+
fields[k] = walk(v);
|
|
193
|
+
return {
|
|
194
|
+
op,
|
|
195
|
+
kind,
|
|
196
|
+
fields,
|
|
197
|
+
strict: true,
|
|
198
|
+
description: OP_DESCRIPTIONS[n],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Look up an enum's values by dotted path. Supported paths include:
|
|
205
|
+
* - 'update.fields.status'
|
|
206
|
+
* - 'update.fields.confidence'
|
|
207
|
+
* - 'update.fields.session_log.type'
|
|
208
|
+
* - 'create-entity.fields.type'
|
|
209
|
+
* - 'create-knowledge.fields.subtype'
|
|
210
|
+
* - 'link.relation'
|
|
211
|
+
*
|
|
212
|
+
* Returns `null` if the path does not resolve to an enum field.
|
|
213
|
+
*/
|
|
214
|
+
export function getEnumValues(path) {
|
|
215
|
+
const parts = path.split('.');
|
|
216
|
+
const opName = parts.shift();
|
|
217
|
+
if (!opName)
|
|
218
|
+
return null;
|
|
219
|
+
const opSchema = getOpSchema(opName);
|
|
220
|
+
if (!opSchema)
|
|
221
|
+
return null;
|
|
222
|
+
let cur = { type: 'object', fields: opSchema.fields, strict: true };
|
|
223
|
+
for (const part of parts) {
|
|
224
|
+
if (!cur || cur.type !== 'object' || !cur.fields)
|
|
225
|
+
return null;
|
|
226
|
+
cur = cur.fields[part];
|
|
227
|
+
}
|
|
228
|
+
if (!cur)
|
|
229
|
+
return null;
|
|
230
|
+
if (cur.type === 'enum' && cur.enum)
|
|
231
|
+
return cur.enum;
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Build a minimal, valid-looking example payload for an op. Uses the first
|
|
236
|
+
* enum value for enums, "<slug>"/"<name>"/"text" placeholders for strings,
|
|
237
|
+
* 0 for numbers, empty arrays for arrays, and recurses for nested objects.
|
|
238
|
+
* Optional fields are skipped unless they are the only way to make the
|
|
239
|
+
* example illustrative (op / kind literals are always emitted).
|
|
240
|
+
*
|
|
241
|
+
* Aliased keys (per contracts.ts ALIAS_KEYS) are stripped from the output
|
|
242
|
+
* so only canonical names appear in rendered help.
|
|
243
|
+
*/
|
|
244
|
+
export function getOpExample(name) {
|
|
245
|
+
if (name === 'digest')
|
|
246
|
+
return structuredClone(DIGEST_EXAMPLE);
|
|
247
|
+
const opSchema = getOpSchema(name);
|
|
248
|
+
if (!opSchema)
|
|
249
|
+
return null;
|
|
250
|
+
const opPath = name; // 'update' | 'create-entity' | ...
|
|
251
|
+
const out = {};
|
|
252
|
+
for (const [k, field] of Object.entries(opSchema.fields)) {
|
|
253
|
+
const fullPath = `${opPath === 'update' ? 'update' : opPath}.${k}`;
|
|
254
|
+
if (shouldStrip(fullPath, k))
|
|
255
|
+
continue;
|
|
256
|
+
if (k === 'fields') {
|
|
257
|
+
if (field.type === 'object' && field.fields) {
|
|
258
|
+
const inner = {};
|
|
259
|
+
for (const [fk, ff] of Object.entries(field.fields)) {
|
|
260
|
+
const fp = aliasPathFor(opSchema.op, opSchema.kind, fk);
|
|
261
|
+
if (shouldStrip(fp, fk))
|
|
262
|
+
continue;
|
|
263
|
+
// Skip most optional fields for minimalism, but always include the
|
|
264
|
+
// primary discriminator-like fields (type for create-entity,
|
|
265
|
+
// subtype for create-knowledge).
|
|
266
|
+
const keepOptional = isDefaultIncluded(opSchema.op, opSchema.kind, fk);
|
|
267
|
+
if (ff.optional && !keepOptional)
|
|
268
|
+
continue;
|
|
269
|
+
inner[fk] = sampleValue(ff);
|
|
270
|
+
}
|
|
271
|
+
out[k] = inner;
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (field.type === 'literal') {
|
|
276
|
+
out[k] = field.literal;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (!field.optional) {
|
|
280
|
+
out[k] = sampleValue(field);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
function aliasPathFor(op, kind, fieldKey) {
|
|
286
|
+
if (op === 'update')
|
|
287
|
+
return `update.fields.${fieldKey}`;
|
|
288
|
+
if (op === 'create' && kind)
|
|
289
|
+
return `create.${kind}.fields.${fieldKey}`;
|
|
290
|
+
return `${op}.${fieldKey}`;
|
|
291
|
+
}
|
|
292
|
+
function shouldStrip(path, _key) {
|
|
293
|
+
return Object.prototype.hasOwnProperty.call(ALIAS_KEYS, path);
|
|
294
|
+
}
|
|
295
|
+
function isDefaultIncluded(op, kind, fieldKey) {
|
|
296
|
+
if (op === 'create' && kind === 'entity' && fieldKey === 'type')
|
|
297
|
+
return true;
|
|
298
|
+
if (op === 'create' && kind === 'knowledge' && (fieldKey === 'subtype' || fieldKey === 'synthesis'))
|
|
299
|
+
return true;
|
|
300
|
+
if (op === 'create' && kind === 'session' && fieldKey === 'synthesis')
|
|
301
|
+
return true;
|
|
302
|
+
if (op === 'update' && fieldKey === 'status')
|
|
303
|
+
return true;
|
|
304
|
+
if (op === 'capture' && fieldKey === 'notes')
|
|
305
|
+
return true;
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
function sampleValue(field) {
|
|
309
|
+
switch (field.type) {
|
|
310
|
+
case 'string': return 'text';
|
|
311
|
+
case 'number': return 0;
|
|
312
|
+
case 'boolean': return true;
|
|
313
|
+
case 'null': return null;
|
|
314
|
+
case 'literal': return field.literal;
|
|
315
|
+
case 'enum': return field.enum?.[0] ?? 'value';
|
|
316
|
+
case 'array': return field.items ? [sampleValue(field.items)] : [];
|
|
317
|
+
case 'object': {
|
|
318
|
+
const o = {};
|
|
319
|
+
if (field.fields) {
|
|
320
|
+
for (const [k, f] of Object.entries(field.fields)) {
|
|
321
|
+
if (f.optional)
|
|
322
|
+
continue;
|
|
323
|
+
o[k] = sampleValue(f);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return o;
|
|
327
|
+
}
|
|
328
|
+
default: return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Flatten an OpSchema to a JSON-schema-shaped plain object for the
|
|
333
|
+
* `hafez schema <op>` command. Not full draft-7 — just a readable
|
|
334
|
+
* structure agents can parse.
|
|
335
|
+
*/
|
|
336
|
+
export function zodToJsonSchema(name) {
|
|
337
|
+
const opSchema = getOpSchema(name);
|
|
338
|
+
if (!opSchema)
|
|
339
|
+
return null;
|
|
340
|
+
return {
|
|
341
|
+
op: opSchema.op,
|
|
342
|
+
kind: opSchema.kind,
|
|
343
|
+
description: opSchema.description,
|
|
344
|
+
fields: fieldsToJson(opSchema.fields),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function fieldsToJson(fields) {
|
|
348
|
+
const out = {};
|
|
349
|
+
for (const [k, f] of Object.entries(fields))
|
|
350
|
+
out[k] = fieldToJson(f);
|
|
351
|
+
return out;
|
|
352
|
+
}
|
|
353
|
+
function fieldToJson(f) {
|
|
354
|
+
const j = { type: f.type };
|
|
355
|
+
if (f.optional)
|
|
356
|
+
j.optional = true;
|
|
357
|
+
if (f.nullable)
|
|
358
|
+
j.nullable = true;
|
|
359
|
+
if (f.enum)
|
|
360
|
+
j.enum = f.enum;
|
|
361
|
+
if (f.literal !== undefined)
|
|
362
|
+
j.literal = f.literal;
|
|
363
|
+
if (f.description)
|
|
364
|
+
j.description = f.description;
|
|
365
|
+
if (f.type === 'array' && f.items)
|
|
366
|
+
j.items = fieldToJson(f.items);
|
|
367
|
+
if (f.type === 'object' && f.fields)
|
|
368
|
+
j.fields = fieldsToJson(f.fields);
|
|
369
|
+
return j;
|
|
370
|
+
}
|
|
371
|
+
// Small re-exports so callers don't need to import from multiple places.
|
|
372
|
+
export { BatchOperationSchema, SessionLogSchema, UpdateFieldsSchema };
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface TypeContract {
|
|
2
|
+
type: string;
|
|
3
|
+
description: string;
|
|
4
|
+
kind: 'item' | 'knowledge';
|
|
5
|
+
requiredFields: string[];
|
|
6
|
+
optionalFields: string[];
|
|
7
|
+
validStatuses: string[];
|
|
8
|
+
defaultSections: string[];
|
|
9
|
+
optionalSections: string[];
|
|
10
|
+
canPromoteTo: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare const ALL_CONTRACTS: TypeContract[];
|
|
13
|
+
export declare function getContract(type: string): TypeContract;
|
|
14
|
+
export declare function hasCapability(type: string, section: string): boolean;
|
|
15
|
+
import type { KnowledgeSubtype } from './types.js';
|
|
16
|
+
export declare const ENTITY_TYPES: readonly ["capture", "entity", "project"];
|
|
17
|
+
export declare const ENTITY_STATUSES: readonly ["active", "paused", "done"];
|
|
18
|
+
export declare const SESSION_LOG_TYPES: readonly ["progress", "decision", "blocker", "research"];
|
|
19
|
+
export declare const CONFIDENCE_LEVELS: readonly ["observation", "pattern", "principle"];
|
|
20
|
+
export declare const VALID_SUBTYPES: readonly ["insight", "plan"];
|
|
21
|
+
export declare const ALIAS_KEYS: {
|
|
22
|
+
readonly 'update.fields.insight': "synthesis";
|
|
23
|
+
readonly 'create.knowledge.fields.insight': "synthesis";
|
|
24
|
+
readonly 'create.session.fields.insight': "synthesis";
|
|
25
|
+
};
|
|
26
|
+
export declare const SUBTYPE_SECTIONS: Record<KnowledgeSubtype, string[]>;
|