hafez 1.3.0 → 1.4.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/dist/batch-ops.js CHANGED
@@ -2,9 +2,13 @@
2
2
  // The Op Spec (see CONTEXT.md): one table defining every batch operation —
3
3
  // shape (Zod), semantic check, description, and example hints — from which
4
4
  // validation, error text, and schema introspection all derive. Adding an op
5
- // means adding one entry here.
5
+ // means one entry here plus one arm in the Apply table (src/apply.ts), which
6
+ // the compiler enforces: the arm table is keyed by this table's names.
7
+ //
8
+ // Every semantic gate lives here and runs exactly once, in the validate phase.
9
+ // Apply assumes the payload it receives passed these checks.
6
10
  import { z } from 'zod';
7
- import { ENTITY_TYPES, ENTITY_STATUSES, SESSION_LOG_TYPES, CONFIDENCE_LEVELS, VALID_SUBTYPES, getContract, } from './contracts.js';
11
+ import { ALIAS_KEYS, ENTITY_TYPES, ENTITY_STATUSES, SESSION_LOG_TYPES, CONFIDENCE_LEVELS, VALID_SUBTYPES, PLAN_STATUSES, getContract, } from './contracts.js';
8
12
  import { slugify } from './vault.js';
9
13
  import { HafezError } from './types.js';
10
14
  // --- Shapes ---
@@ -19,15 +23,29 @@ export const SessionLogSchema = z.object({
19
23
  body: z.string().optional(),
20
24
  agent: z.string(),
21
25
  }).strict();
26
+ // Body-section text must have text after trimming. An empty or whitespace-only
27
+ // value (an agent expanding an unset variable) used to sail through the shape —
28
+ // on the action/evidence fields it was dropped by a truthiness guard in apply
29
+ // (exit 0, nothing on disk); on the section-setting fields below it was worse,
30
+ // because apply trusts the validate phase and `setSection(body, 'Synthesis',
31
+ // '')` wipes what was already there. Same false-success class the CLI loop
32
+ // closed for the enum flags, once silent and once destructive.
33
+ //
34
+ // The rule: every non-nullable string field that writes body-section text is
35
+ // nonEmptyText. The nullable ones (update's brief/description/resource) are
36
+ // excluded by construction — null is their documented clear signal, and the
37
+ // CLI maps `--brief ""` to it. Trimming also keeps accidental padding out of
38
+ // stored text.
39
+ const nonEmptyText = z.string().trim().min(1, 'must contain text — an empty value is rejected, never silently dropped');
22
40
  export const UpdateFieldsSchema = z.object({
23
41
  status: z.enum([...ENTITY_STATUSES]).optional(),
24
- current_state: z.string().optional(),
42
+ current_state: nonEmptyText.optional(),
25
43
  session_log: SessionLogSchema.optional(),
26
44
  brief: z.string().nullable().optional(),
27
- add_action: z.string().optional(),
28
- add_actions: z.array(z.string()).optional(),
29
- complete_action: z.string().optional(),
30
- remove_action: z.string().optional(),
45
+ add_action: nonEmptyText.optional(),
46
+ add_actions: z.array(nonEmptyText).optional(),
47
+ complete_action: nonEmptyText.optional(),
48
+ remove_action: nonEmptyText.optional(),
31
49
  clear_actions: z.boolean().optional(),
32
50
  // Knowledge metadata
33
51
  description: z.string().nullable().optional(),
@@ -36,28 +54,31 @@ export const UpdateFieldsSchema = z.object({
36
54
  confidence: z.enum([...CONFIDENCE_LEVELS]).optional(),
37
55
  tags: z.array(z.string()).optional(),
38
56
  related: z.array(z.string()).optional(),
39
- synthesis: z.string().optional(),
40
- insight: z.string().optional(), // alias for synthesis (normalized in parseBatchInput)
41
- add_evidence: z.string().optional(),
42
- add_source: z.string().optional(),
57
+ synthesis: nonEmptyText.optional(),
58
+ insight: nonEmptyText.optional(), // alias for synthesis (normalized by normalizeAliases)
59
+ add_evidence: nonEmptyText.optional(),
60
+ add_source: nonEmptyText.optional(),
43
61
  }).strict();
44
62
  export const CreateEntityFieldsSchema = z.object({
45
63
  type: z.enum([...ENTITY_TYPES]),
46
- purpose: z.string().optional(),
64
+ purpose: nonEmptyText.optional(),
47
65
  description: z.string().optional(),
48
66
  resource: z.string().optional(),
49
67
  domain: z.array(z.string()).optional(),
50
68
  parent: z.string().optional(),
51
69
  related: z.array(z.string()).optional(),
52
70
  tags: z.array(z.string()).optional(),
53
- brief: z.string().optional(),
54
- add_action: z.string().optional(),
55
- add_actions: z.array(z.string()).optional(),
71
+ brief: nonEmptyText.optional(),
72
+ add_action: nonEmptyText.optional(),
73
+ add_actions: z.array(nonEmptyText).optional(),
56
74
  }).strict();
57
75
  export const CreateKnowledgeFieldsSchema = z.object({
58
76
  subtype: z.enum([...VALID_SUBTYPES]).optional(),
59
- synthesis: z.string().optional(),
60
- insight: z.string().optional(),
77
+ // Plan-only: insights have no lifecycle, so the semantic check rejects
78
+ // status on any other subtype (the shape only pins the allowed values).
79
+ status: z.enum([...PLAN_STATUSES]).optional(),
80
+ synthesis: nonEmptyText.optional(),
81
+ insight: nonEmptyText.optional(),
61
82
  description: z.string().optional(),
62
83
  domain: z.array(z.string()).optional(),
63
84
  related: z.array(z.string()).optional(),
@@ -65,8 +86,8 @@ export const CreateKnowledgeFieldsSchema = z.object({
65
86
  'session-date': z.string().optional(),
66
87
  }).strict();
67
88
  export const CreateSessionFieldsSchema = z.object({
68
- synthesis: z.string().optional(),
69
- insight: z.string().optional(),
89
+ synthesis: nonEmptyText.optional(),
90
+ insight: nonEmptyText.optional(),
70
91
  related: z.array(z.string()).optional(),
71
92
  'session-date': z.string().optional(),
72
93
  }).strict();
@@ -134,25 +155,124 @@ export const BatchOperationSchema = z.discriminatedUnion('op', [
134
155
  UnlinkBranch,
135
156
  PromoteBranch,
136
157
  ]);
158
+ // --- Alias normalization ---
159
+ // The one place a declared alias (contracts.ts ALIAS_KEYS) becomes its canonical
160
+ // field. It runs before validation, so validate and apply both see the canonical
161
+ // op on every entry point — library create/update/batch and CLI batch input
162
+ // alike. Normalizing anywhere else is what made the alias a silent drop: the
163
+ // strict shapes accepted `insight`, nothing rewrote it, and the apply step (which
164
+ // only reads `synthesis`) wrote an empty Synthesis section and reported ok.
165
+ /** ALIAS_KEYS re-keyed by op path prefix: 'update.fields' → [[alias, canonical]]. */
166
+ const ALIASES_BY_PATH = new Map();
167
+ for (const [path, canonical] of Object.entries(ALIAS_KEYS)) {
168
+ const cut = path.lastIndexOf('.');
169
+ const prefix = path.slice(0, cut);
170
+ const list = ALIASES_BY_PATH.get(prefix) ?? [];
171
+ list.push([path.slice(cut + 1), canonical]);
172
+ ALIASES_BY_PATH.set(prefix, list);
173
+ }
174
+ /** Canonicalize an operation's declared aliases.
175
+ *
176
+ * Precedence when both are present: the canonical field wins and the alias is
177
+ * dropped. Never mutates the caller's object — an op carrying no alias is
178
+ * returned as-is. */
179
+ export function normalizeAliases(op) {
180
+ const o = op;
181
+ if (!o || typeof o !== 'object' || !o.fields || typeof o.fields !== 'object')
182
+ return op;
183
+ const prefix = `${String(o.op)}${o.kind === undefined ? '' : `.${String(o.kind)}`}.fields`;
184
+ const aliases = ALIASES_BY_PATH.get(prefix);
185
+ if (!aliases)
186
+ return op;
187
+ let fields;
188
+ for (const [alias, canonical] of aliases) {
189
+ if (o.fields[alias] === undefined)
190
+ continue;
191
+ fields ??= { ...o.fields };
192
+ if (fields[canonical] === undefined)
193
+ fields[canonical] = fields[alias];
194
+ delete fields[alias];
195
+ }
196
+ return fields ? { ...o, fields } : op;
197
+ }
198
+ /** normalizeAliases over a payload — the form the Spine and dry-run use. */
199
+ export function normalizeOperations(operations) {
200
+ return operations.map(op => normalizeAliases(op));
201
+ }
137
202
  function pushErr(errors, err) {
138
203
  const e = err;
139
204
  errors.push(e.details?.length ? `${e.message}: ${e.details.join('; ')}` : e.message);
140
205
  }
141
- export const OP_SPECS = [
206
+ /** The one source of the missing-slug error text — specs and the Spine's
207
+ * shape-failure precedence rule (validateBatchCore) must not drift apart. */
208
+ export function notFoundError(slug) {
209
+ return `'${slug}' not found`;
210
+ }
211
+ /** The one op-error line format. Every surface that localises an error to its
212
+ * op — the validate phase's error list, apply's execution-time failures, and
213
+ * the CLI's dry-run report — renders it through here, so an agent parsing
214
+ * `op[i] (op slug): …` sees one shape whichever surface produced it. */
215
+ export function opErrorLine(index, op, slug, message) {
216
+ return `op[${index}] (${op} ${slug}): ${message}`;
217
+ }
218
+ /** The one "the batch was rejected, nothing landed" header — shared by the
219
+ * library's validate gate and the CLI's shape-parse gate, which reject the
220
+ * same payload at different depths and must say so identically. `batch
221
+ * --dry-run` deliberately words its own header differently: it reports a
222
+ * hypothetical ("would be applied"), not a refusal. */
223
+ export function batchInvalidHeader(invalidCount, total) {
224
+ return `Batch failed validation: ${invalidCount} invalid operation${invalidCount === 1 ? '' : 's'} (0 of ${total} applied)`;
225
+ }
226
+ /** The one source of the duplicate-session disposition text. A duplicate
227
+ * session is not an error — the op is skipped and the batch continues — and
228
+ * dry-run (spec warning) and apply (BatchResult.warning) report it with this
229
+ * same sentence so the two can never drift. */
230
+ export function duplicateSessionWarning(slug) {
231
+ return `session '${slug}' already exists — op skipped (batch continues)`;
232
+ }
233
+ /** The create-style gate, in one place so the invariant can't be re-broken:
234
+ * a taken slug is a SLUG_EXISTS error and must NOT be simulated (simulating
235
+ * over an existing slug shadows the real file's kind for later ops in the
236
+ * batch). The builder runs before the claim — it must see the pre-claim
237
+ * world so a self-referencing parent still fails. */
238
+ function claimNew(ctx, errors, slug, info, build) {
239
+ const taken = ctx.existsAnywhere(slug);
240
+ if (taken)
241
+ errors.push(`Slug '${slug}' already exists`);
242
+ if (build) {
243
+ try {
244
+ build();
245
+ }
246
+ catch (err) {
247
+ pushErr(errors, err);
248
+ }
249
+ }
250
+ if (!taken)
251
+ ctx.simulate(slug, info);
252
+ return taken ? 'SLUG_EXISTS' : undefined;
253
+ }
254
+ // `as const satisfies` rather than a `: OpSpec[]` annotation: it keeps each
255
+ // row's `name` a literal, which is what OpSpecName (and through it the Apply
256
+ // table's key type) is derived from. OP_SPECS below re-exports the rows under
257
+ // the interface, so consumers still read optional fields (`kind`) off them.
258
+ const OP_SPEC_ROWS = [
142
259
  {
143
260
  name: 'update',
144
261
  op: 'update',
145
262
  schema: UpdateBranch,
146
- description: 'Update an entity or knowledge note (status, brief, next actions, session log, synthesis, …).',
263
+ description: 'Update an entity or knowledge note (status, brief, next actions, session log, synthesis, …). `insight` is an alias for `synthesis` (knowledge-only; ignored when `synthesis` is also given). On knowledge notes `status` is plan-subtype only (active|done); insights reject it.',
147
264
  exampleInclude: ['status'],
148
265
  check(op, ctx) {
149
266
  const errors = [];
267
+ let primaryCode;
150
268
  const target = ctx.resolve(op.slug);
151
- if (!target)
152
- errors.push(`'${op.slug}' not found`);
269
+ if (!target) {
270
+ errors.push(notFoundError(op.slug));
271
+ primaryCode = 'NOT_FOUND';
272
+ }
153
273
  else {
154
274
  try {
155
- ctx.validateKindFields(target.kind, op.fields);
275
+ ctx.validateKindFields(target.kind, op.fields, target.subtype);
156
276
  }
157
277
  catch (err) {
158
278
  pushErr(errors, err);
@@ -169,7 +289,7 @@ export const OP_SPECS = [
169
289
  errors.push(`Related slug '${r}' does not exist`);
170
290
  }
171
291
  }
172
- return { slug: op.slug, errors };
292
+ return { slug: op.slug, errors, primaryCode };
173
293
  },
174
294
  },
175
295
  {
@@ -177,21 +297,13 @@ export const OP_SPECS = [
177
297
  op: 'create',
178
298
  kind: 'entity',
179
299
  schema: CreateEntityBranch,
180
- description: 'Create a new entity (capture / entity / project).',
300
+ description: 'Create a new entity (capture / entity / project). A field whose section the chosen type lacks is rejected, never dropped — `purpose` needs type project.',
181
301
  exampleInclude: ['type'],
182
302
  check(op, ctx) {
183
303
  const errors = [];
184
304
  const slug = slugify(op.name);
185
- if (ctx.existsAnywhere(slug))
186
- errors.push(`Slug '${slug}' already exists`);
187
- try {
188
- ctx.buildEntityFrontmatter(op.name, op.fields);
189
- }
190
- catch (err) {
191
- pushErr(errors, err);
192
- }
193
- ctx.simulate(slug, { kind: 'entity', type: op.fields.type });
194
- return { slug, errors, created: errors.length === 0 };
305
+ const primaryCode = claimNew(ctx, errors, slug, { kind: 'entity', type: op.fields.type }, () => ctx.buildEntityFrontmatter(op.name, op.fields));
306
+ return { slug, errors, created: errors.length === 0, primaryCode };
195
307
  },
196
308
  },
197
309
  {
@@ -199,21 +311,13 @@ export const OP_SPECS = [
199
311
  op: 'create',
200
312
  kind: 'knowledge',
201
313
  schema: CreateKnowledgeBranch,
202
- description: 'Create a new knowledge note (insight / plan subtype).',
314
+ description: 'Create a new knowledge note (insight / plan subtype). `insight` is an alias for `synthesis` (ignored when `synthesis` is also given). `status` (active|done) is plan-only; `synthesis` is insight-only — plan bodies are Goal/Steps/Dependencies, and a field whose section the subtype lacks is rejected, never dropped.',
203
315
  exampleInclude: ['subtype', 'synthesis'],
204
316
  check(op, ctx) {
205
317
  const errors = [];
206
318
  const slug = slugify(op.name);
207
- if (ctx.existsAnywhere(slug))
208
- errors.push(`Slug '${slug}' already exists`);
209
- try {
210
- ctx.buildKnowledgeFrontmatter(op.name, (op.fields || {}));
211
- }
212
- catch (err) {
213
- pushErr(errors, err);
214
- }
215
- ctx.simulate(slug, { kind: 'knowledge', type: 'knowledge' });
216
- return { slug, errors, created: errors.length === 0 };
319
+ const primaryCode = claimNew(ctx, errors, slug, { kind: 'knowledge', type: 'knowledge', subtype: op.fields?.subtype }, () => ctx.buildKnowledgeFrontmatter(op.name, (op.fields || {})));
320
+ return { slug, errors, created: errors.length === 0, primaryCode };
217
321
  },
218
322
  },
219
323
  {
@@ -221,12 +325,12 @@ export const OP_SPECS = [
221
325
  op: 'create',
222
326
  kind: 'session',
223
327
  schema: CreateSessionBranch,
224
- description: 'Create a new session note linked to an entity.',
328
+ description: 'Create a new session note linked to an entity. `insight` is an alias for `synthesis` (ignored when `synthesis` is also given).',
225
329
  exampleInclude: ['synthesis'],
226
330
  check(op, ctx) {
227
331
  const slug = slugify(op.name);
228
332
  if (ctx.existsAnywhere(slug) || ctx.sessionExists(slug)) {
229
- return { slug, errors: [], warning: `session '${slug}' already exists — op will be skipped (batch continues)` };
333
+ return { slug, errors: [], warning: duplicateSessionWarning(slug) };
230
334
  }
231
335
  ctx.simulateSession(slug);
232
336
  return { slug, errors: [], created: true };
@@ -241,10 +345,8 @@ export const OP_SPECS = [
241
345
  check(op, ctx) {
242
346
  const errors = [];
243
347
  const slug = slugify(op.name);
244
- if (ctx.existsAnywhere(slug))
245
- errors.push(`Slug '${slug}' already exists`);
246
- ctx.simulate(slug, { kind: 'entity', type: 'capture' });
247
- return { slug, errors, created: errors.length === 0 };
348
+ const primaryCode = claimNew(ctx, errors, slug, { kind: 'entity', type: 'capture' });
349
+ return { slug, errors, created: errors.length === 0, primaryCode };
248
350
  },
249
351
  },
250
352
  {
@@ -255,11 +357,14 @@ export const OP_SPECS = [
255
357
  exampleInclude: [],
256
358
  check(op, ctx) {
257
359
  const errors = [];
258
- if (!ctx.resolve(op.slug))
259
- errors.push(`'${op.slug}' not found`);
360
+ let primaryCode;
361
+ if (!ctx.resolve(op.slug)) {
362
+ errors.push(notFoundError(op.slug));
363
+ primaryCode = 'NOT_FOUND';
364
+ }
260
365
  if (!ctx.existsAnywhere(op.target))
261
366
  errors.push(`Target '${op.target}' does not exist`);
262
- return { slug: op.slug, errors };
367
+ return { slug: op.slug, errors, primaryCode };
263
368
  },
264
369
  },
265
370
  {
@@ -270,9 +375,12 @@ export const OP_SPECS = [
270
375
  exampleInclude: [],
271
376
  check(op, ctx) {
272
377
  const errors = [];
273
- if (!ctx.resolve(op.slug))
274
- errors.push(`'${op.slug}' not found`);
275
- return { slug: op.slug, errors };
378
+ let primaryCode;
379
+ if (!ctx.resolve(op.slug)) {
380
+ errors.push(notFoundError(op.slug));
381
+ primaryCode = 'NOT_FOUND';
382
+ }
383
+ return { slug: op.slug, errors, primaryCode };
276
384
  },
277
385
  },
278
386
  {
@@ -283,9 +391,12 @@ export const OP_SPECS = [
283
391
  exampleInclude: [],
284
392
  check(op, ctx) {
285
393
  const errors = [];
394
+ let primaryCode;
286
395
  const cur = ctx.resolve(op.slug);
287
- if (!cur)
288
- errors.push(`'${op.slug}' not found`);
396
+ if (!cur) {
397
+ errors.push(notFoundError(op.slug));
398
+ primaryCode = 'NOT_FOUND';
399
+ }
289
400
  else {
290
401
  const currentType = cur.kind === 'knowledge' ? 'knowledge' : cur.type;
291
402
  try {
@@ -301,10 +412,11 @@ export const OP_SPECS = [
301
412
  pushErr(errors, err);
302
413
  }
303
414
  }
304
- return { slug: op.slug, errors };
415
+ return { slug: op.slug, errors, primaryCode };
305
416
  },
306
417
  },
307
418
  ];
419
+ export const OP_SPECS = OP_SPEC_ROWS;
308
420
  /** Find the spec for a (shape-valid) operation. */
309
421
  export function specFor(op) {
310
422
  const spec = OP_SPECS.find(s => s.op === op.op && (s.kind === undefined || s.kind === op.kind));
@@ -316,6 +428,18 @@ export function specFor(op) {
316
428
  export function validOpNames() {
317
429
  return [...new Set(OP_SPECS.map(s => s.op))];
318
430
  }
431
+ /** Ops whose payload addresses an existing file by `slug` — derived from the
432
+ * Zod shapes, so a new slug-bearing op picks up slug-level rules (the
433
+ * NOT_FOUND precedence on shape failure in validateBatchCore) without a
434
+ * hand-kept list. */
435
+ export function slugBearingOpNames() {
436
+ return [...new Set(OP_SPECS
437
+ .filter(s => {
438
+ const shape = s.schema.shape;
439
+ return shape !== undefined && 'slug' in shape;
440
+ })
441
+ .map(s => s.op))];
442
+ }
319
443
  /** Create kinds for error text — derived from the table. */
320
444
  export function validCreateKinds() {
321
445
  return OP_SPECS.filter(s => s.op === 'create' && s.kind).map(s => s.kind);
@@ -10,6 +10,10 @@ export interface ParsedArgs {
10
10
  positionals: string[];
11
11
  }
12
12
  export declare function parseArgs(args: string[], spec: FlagSpec, usage: string): ParsedArgs;
13
- /** Comma-separated list flag → trimmed array (undefined stays undefined). */
13
+ /** The one comma-list parser — every list flag on every command goes through
14
+ * it. Trims each segment and drops the empty ones, so `"ai, ml"` is two
15
+ * domains (not `ai` and ` ml`, which no query matches) and `"eng,"` is one
16
+ * (not one plus an empty string persisted to frontmatter and the index).
17
+ * undefined stays undefined; an all-empty list is an empty array (a clear). */
14
18
  export declare function splitList(value: string | boolean | undefined): string[] | undefined;
15
19
  export declare function str(value: string | boolean | undefined): string | undefined;
package/dist/cli/args.js CHANGED
@@ -38,11 +38,15 @@ export function parseArgs(args, spec, usage) {
38
38
  }
39
39
  return { flags, positionals };
40
40
  }
41
- /** Comma-separated list flag → trimmed array (undefined stays undefined). */
41
+ /** The one comma-list parser — every list flag on every command goes through
42
+ * it. Trims each segment and drops the empty ones, so `"ai, ml"` is two
43
+ * domains (not `ai` and ` ml`, which no query matches) and `"eng,"` is one
44
+ * (not one plus an empty string persisted to frontmatter and the index).
45
+ * undefined stays undefined; an all-empty list is an empty array (a clear). */
42
46
  export function splitList(value) {
43
47
  if (typeof value !== 'string')
44
48
  return undefined;
45
- return value.split(',').map(s => s.trim());
49
+ return value.split(',').map(s => s.trim()).filter(Boolean);
46
50
  }
47
51
  export function str(value) {
48
52
  return typeof value === 'string' ? value : undefined;
@@ -1,11 +1,34 @@
1
+ import { type FlagSpec } from './args.js';
1
2
  import type { Hafez, BatchOperation } from '../types.js';
2
3
  export interface CommandOpts {
3
4
  json?: boolean;
5
+ /** Resolved vault path — for handlers that read the vault directly
6
+ * (export) rather than through the Hafez instance. */
7
+ vaultPath?: string;
4
8
  }
5
9
  export declare function cmdRead(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
10
+ /** Every flag the query parser accepts, for the branch-support matrix. */
11
+ export declare const QUERY_FLAG_NAMES: string[];
12
+ export type QueryBranch = 'entity' | 'knowledge' | 'all' | 'related-to';
13
+ /** Which flags each kind branch actually applies. A branch that cannot honour
14
+ * a flag rejects it (below) instead of dropping it: `--kind knowledge --filter
15
+ * stale` used to return every knowledge note with exit 0, and `--confidence`
16
+ * never reached an entity query at all. */
17
+ export declare const QUERY_FLAG_SUPPORT: Record<QueryBranch, readonly string[]>;
6
18
  export declare function cmdQuery(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
7
19
  export declare function cmdSearch(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
20
+ /** Exported so tests can assert the flag × kind matrix is exhaustive. */
21
+ export declare const CREATE_FLAGS: FlagSpec;
8
22
  export declare function cmdCreate(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
23
+ /** Exported so tests can assert the table covers the update schema. */
24
+ export declare const UPDATE_FLAGS: FlagSpec;
25
+ /** Flags the generic loop skips because they are not a field of the update
26
+ * shape — each is hand-handled in cmdUpdate. */
27
+ export declare const UPDATE_SPECIAL_FLAGS: Set<string>;
28
+ /** Update-shape fields with no flag of their own, and why. The exhaustiveness
29
+ * test consults this, so a gap here is a stated decision rather than the
30
+ * silent hole `add_actions` was. */
31
+ export declare const UPDATE_FIELDS_WITHOUT_FLAGS: Record<string, string>;
9
32
  export declare function cmdUpdate(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
10
33
  export declare function cmdLink(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
11
34
  export declare function cmdUnlink(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
@@ -19,3 +42,4 @@ export declare function cmdChangelog(os: Hafez, args: string[], opts?: CommandOp
19
42
  export declare function parseBatchInput(json: string): BatchOperation[];
20
43
  export declare function cmdBatch(os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;
21
44
  export declare function cmdDigest(os: Hafez, args: string[], _opts?: CommandOpts): Promise<string>;
45
+ export declare function cmdExport(_os: Hafez, args: string[], opts?: CommandOpts): Promise<string>;