iranti 0.3.38 → 0.3.40

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.
@@ -9,7 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  *
10
10
  * Provides commands: install, setup, configure, auth, doctor, run, attend,
11
11
  * handshake, resolve, integrate, project-init, instance, status, upgrade,
12
- * uninstall, issues, list-rules, delete-rule, and more.
12
+ * uninstall, issues, list-rules, delete-rule, export, import, snapshot, and more.
13
13
  *
14
14
  * Run via `npx iranti <command>` or `iranti <command>` after global install.
15
15
  * Requires DATABASE_URL or a project/instance binding for most commands.
@@ -19,7 +19,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
19
19
  *
20
20
  * Provides commands: install, setup, configure, auth, doctor, run, attend,
21
21
  * handshake, resolve, integrate, project-init, instance, status, upgrade,
22
- * uninstall, issues, list-rules, delete-rule, and more.
22
+ * uninstall, issues, list-rules, delete-rule, export, import, snapshot, and more.
23
23
  *
24
24
  * Run via `npx iranti <command>` or `iranti <command>` after global install.
25
25
  * Requires DATABASE_URL or a project/instance binding for most commands.
@@ -55,6 +55,7 @@ const semanticFactTags_1 = require("../src/lib/semanticFactTags");
55
55
  const staffEventRegistry_1 = require("../src/lib/staffEventRegistry");
56
56
  const scaffoldCloseout_1 = require("../src/lib/scaffoldCloseout");
57
57
  const projectLearning_1 = require("../src/lib/projectLearning");
58
+ const feedbackCollector_1 = require("../src/lib/feedbackCollector");
58
59
  class CliError extends Error {
59
60
  constructor(code, message, hints = [], details) {
60
61
  super(message);
@@ -8587,6 +8588,9 @@ function printResolveHelp() {
8587
8588
  function printProviderKeyHelp() {
8588
8589
  (0, cliHelpRenderer_1.printProviderKeyHelp)({ sectionTitle, commandText });
8589
8590
  }
8591
+ function printFeedbackHelp() {
8592
+ (0, cliHelpRenderer_1.printFeedbackHelp)({ sectionTitle, commandText });
8593
+ }
8590
8594
  function printMcpHelp() {
8591
8595
  console.log([
8592
8596
  'MCP server and maintenance commands.',
@@ -9005,6 +9009,504 @@ async function deleteRuleCommand(args) {
9005
9009
  await (0, client_1.disconnectDb)().catch(() => undefined);
9006
9010
  }
9007
9011
  }
9012
+ // ── Export / Import / Snapshot commands ─────────────────────────────────────
9013
+ /** Marker file written at the end of every successful export (enables --since last). */
9014
+ const EXPORT_MARKER_FILE = '.iranti-export-marker';
9015
+ /** Directory where `iranti snapshot create` stores named JSONL snapshots. */
9016
+ const SNAPSHOT_DIR = path_1.default.join('.iranti', 'snapshots');
9017
+ function printExportHelp() {
9018
+ console.log([
9019
+ 'Export knowledge base facts to JSONL format.',
9020
+ '',
9021
+ 'Usage:',
9022
+ ' iranti export [--output <file>] [--entity-type <type>] [--entity <type/id>]',
9023
+ ' [--since <duration|ISO|last>] [--instance <name>] [--project-env <path>]',
9024
+ ' [--json]',
9025
+ '',
9026
+ 'Flags:',
9027
+ ' --output <file> Write JSONL to this file instead of stdout.',
9028
+ ' --entity-type <type> Only export facts of this entityType (e.g. rule, project).',
9029
+ ' --entity <type/id> Only export facts for this entity (e.g. project/myapp).',
9030
+ ' --since <value> Incremental export. Value can be:',
9031
+ ' - ISO-8601 timestamp: 2025-01-01T00:00:00Z',
9032
+ ' - Duration ago: 1h, 2d, 30m',
9033
+ ' - "last": resume from previous export marker',
9034
+ ' --json Print summary as JSON to stderr.',
9035
+ ' --instance <name> Use a named Iranti instance.',
9036
+ ' --project-env <path> Path to a .env file with DATABASE_URL.',
9037
+ '',
9038
+ 'Examples:',
9039
+ ' iranti export # full export to stdout',
9040
+ ' iranti export --output backup.jsonl # full export to file',
9041
+ ' iranti export --entity-type rule # export only rules',
9042
+ ' iranti export --since last # export changes since last run',
9043
+ ' iranti export --since 1d # export changes from the last 24h',
9044
+ ].join('\n'));
9045
+ }
9046
+ function printImportHelp() {
9047
+ console.log([
9048
+ 'Import JSONL facts into the knowledge base.',
9049
+ '',
9050
+ 'Usage:',
9051
+ ' iranti import <file> [--conflict skip|overwrite|merge]',
9052
+ ' [--dry-run] [--remap old=new] [--provenance <tag>]',
9053
+ ' [--instance <name>] [--project-env <path>] [--json]',
9054
+ '',
9055
+ 'Flags:',
9056
+ ' <file> JSONL file to import (use "-" for stdin).',
9057
+ ' --conflict <mode> How to handle existing entries:',
9058
+ ' skip (default) leave existing entries unchanged',
9059
+ ' overwrite always replace with the imported value',
9060
+ ' merge keep whichever entry has higher confidence',
9061
+ ' --dry-run Parse and count without writing to the database.',
9062
+ ' --remap <old>=<new> Rewrite entityType (or entityType/entityId) on import.',
9063
+ ' Repeatable. e.g. --remap rule=policy',
9064
+ ' --remap project/foo=project/bar',
9065
+ ' --provenance <tag> Tag stamped on every imported entry as importedFrom.',
9066
+ ' Allows targeted rollback later.',
9067
+ ' --json Print result as JSON.',
9068
+ ' --instance <name> Use a named Iranti instance.',
9069
+ ' --project-env <path> Path to a .env file with DATABASE_URL.',
9070
+ '',
9071
+ 'Examples:',
9072
+ ' iranti import backup.jsonl',
9073
+ ' iranti import backup.jsonl --conflict merge',
9074
+ ' iranti import backup.jsonl --dry-run',
9075
+ ' iranti import backup.jsonl --remap rule=policy --provenance backup-2025-01-01',
9076
+ ].join('\n'));
9077
+ }
9078
+ function printSnapshotHelp() {
9079
+ console.log([
9080
+ 'Manage named knowledge base snapshots stored in .iranti/snapshots/.',
9081
+ '',
9082
+ 'Usage:',
9083
+ ' iranti snapshot <subcommand> [options]',
9084
+ '',
9085
+ 'Subcommands:',
9086
+ ' create <name> Export the full knowledge base to .iranti/snapshots/<name>.jsonl',
9087
+ ' restore <name> Import a snapshot (default conflict mode: overwrite)',
9088
+ ' list List all saved snapshots',
9089
+ ' delete <name> Delete a saved snapshot',
9090
+ '',
9091
+ 'Flags (create/restore):',
9092
+ ' --conflict <mode> Conflict mode for restore (skip|overwrite|merge). Default: overwrite.',
9093
+ ' --dry-run Preview restore without writing.',
9094
+ ' --instance <name> Use a named Iranti instance.',
9095
+ ' --project-env <path>',
9096
+ '',
9097
+ 'Examples:',
9098
+ ' iranti snapshot create pre-refactor',
9099
+ ' iranti snapshot list',
9100
+ ' iranti snapshot restore pre-refactor',
9101
+ ' iranti snapshot restore pre-refactor --dry-run',
9102
+ ' iranti snapshot delete pre-refactor',
9103
+ ].join('\n'));
9104
+ }
9105
+ /**
9106
+ * Shared DB resolver for export/import commands. Identical logic to
9107
+ * resolveDbForRuleCommands but with a distinct applicationName so query
9108
+ * logs are easy to attribute.
9109
+ */
9110
+ async function resolveDbForExportCommands(args) {
9111
+ const instanceName = getFlag(args, 'instance');
9112
+ if (instanceName) {
9113
+ const scope = normalizeScope(getFlag(args, 'scope'));
9114
+ const root = resolveInstallRoot(args, scope);
9115
+ const loaded = await loadInstanceEnv(root, instanceName);
9116
+ applyEnvMap(loaded.env);
9117
+ }
9118
+ else {
9119
+ const cwd = path_1.default.resolve(getFlag(args, 'project') ?? process.cwd());
9120
+ const explicitProjectEnv = getFlag(args, 'project-env');
9121
+ (0, runtimeEnv_1.loadRuntimeEnv)({
9122
+ cwd,
9123
+ projectEnvFile: explicitProjectEnv ? path_1.default.resolve(explicitProjectEnv) : undefined,
9124
+ });
9125
+ }
9126
+ const databaseUrl = process.env.DATABASE_URL?.trim();
9127
+ if (!databaseUrl) {
9128
+ throw cliError('IRANTI_DATABASE_URL_MISSING', 'DATABASE_URL is required. Run from a bound project, pass --instance <name>, or set DATABASE_URL.', ['Run `iranti project init . --instance <name>` to bind this project.']);
9129
+ }
9130
+ (0, client_1.initDb)(databaseUrl, { applicationName: 'iranti:cli:export' });
9131
+ }
9132
+ /**
9133
+ * Parse a --since flag value into a Date.
9134
+ * Accepts ISO timestamps, durations (15m / 2h / 1d), and the special
9135
+ * value "last" which reads the .iranti-export-marker file from cwd.
9136
+ */
9137
+ function parseSinceFlag(value, cwd) {
9138
+ const trimmed = value.trim();
9139
+ if (trimmed === 'last') {
9140
+ const markerPath = path_1.default.join(cwd, EXPORT_MARKER_FILE);
9141
+ if (!fs_1.default.existsSync(markerPath)) {
9142
+ throw cliError('IRANTI_EXPORT_MARKER_MISSING', `No export marker found at ${markerPath}. Run a full export first to create one.`, ['Run: iranti export --output backup.jsonl']);
9143
+ }
9144
+ const raw = fs_1.default.readFileSync(markerPath, 'utf8').trim();
9145
+ const d = new Date(raw);
9146
+ if (Number.isNaN(d.getTime())) {
9147
+ throw cliError('IRANTI_EXPORT_MARKER_INVALID', `Export marker at ${markerPath} contains an invalid timestamp: "${raw}".`, ['Delete the marker file and run a full export.']);
9148
+ }
9149
+ return d;
9150
+ }
9151
+ // Try duration (15m, 2h, 1d, 30s, 500ms)
9152
+ if (/^[\d]+\s*(ms|s|m|h|d)?$/i.test(trimmed)) {
9153
+ const ms = parseDurationFlag(trimmed, 'since');
9154
+ return new Date(Date.now() - ms);
9155
+ }
9156
+ // Try ISO timestamp
9157
+ const d = new Date(trimmed);
9158
+ if (Number.isNaN(d.getTime())) {
9159
+ throw cliError('IRANTI_SINCE_INVALID', `Invalid --since value: "${trimmed}". Use an ISO timestamp, a duration (15m, 2h, 1d), or "last".`, ['Example: --since 2025-01-01T00:00:00Z', 'Example: --since 1d', 'Example: --since last']);
9160
+ }
9161
+ return d;
9162
+ }
9163
+ async function exportCommand(args) {
9164
+ try {
9165
+ const json = hasFlag(args, 'json');
9166
+ const outputFile = getFlag(args, 'output');
9167
+ const entityTypeFlag = getFlag(args, 'entity-type');
9168
+ const entityFlag = getFlag(args, 'entity');
9169
+ const sinceFlag = getFlag(args, 'since');
9170
+ const cwd = path_1.default.resolve(getFlag(args, 'project') ?? process.cwd());
9171
+ let entityType = entityTypeFlag ?? undefined;
9172
+ let entityId;
9173
+ if (entityFlag) {
9174
+ const slash = entityFlag.indexOf('/');
9175
+ if (slash < 1) {
9176
+ throw cliError('IRANTI_EXPORT_ENTITY_INVALID', `--entity must be in "type/id" format, got: "${entityFlag}"`, ['Example: --entity project/myapp']);
9177
+ }
9178
+ entityType = entityFlag.slice(0, slash);
9179
+ entityId = entityFlag.slice(slash + 1);
9180
+ }
9181
+ let since;
9182
+ if (sinceFlag) {
9183
+ since = parseSinceFlag(sinceFlag, cwd);
9184
+ }
9185
+ await resolveDbForExportCommands(args);
9186
+ const rows = await (0, queries_1.exportFacts)({ entityType, entityId, since });
9187
+ const exportedAt = new Date().toISOString();
9188
+ const header = { _type: 'iranti-export', version: '1', exportedAt, total: rows.length };
9189
+ const lines = [JSON.stringify(header), ...rows.map((r) => JSON.stringify(r))];
9190
+ const output = lines.join('\n') + '\n';
9191
+ if (outputFile) {
9192
+ fs_1.default.mkdirSync(path_1.default.dirname(path_1.default.resolve(outputFile)), { recursive: true });
9193
+ fs_1.default.writeFileSync(path_1.default.resolve(outputFile), output, 'utf8');
9194
+ }
9195
+ else {
9196
+ process.stdout.write(output);
9197
+ }
9198
+ // Write export marker for --since last
9199
+ if (!since) {
9200
+ // Only stamp the marker on full (non-incremental) exports so that
9201
+ // --since last always covers the delta from the last FULL export.
9202
+ try {
9203
+ fs_1.default.writeFileSync(path_1.default.join(cwd, EXPORT_MARKER_FILE), exportedAt + '\n', 'utf8');
9204
+ }
9205
+ catch {
9206
+ // Marker write is best-effort; don't fail the export.
9207
+ }
9208
+ }
9209
+ if (json) {
9210
+ process.stderr.write(JSON.stringify({
9211
+ ok: true,
9212
+ total: rows.length,
9213
+ exportedAt,
9214
+ output: outputFile ?? '(stdout)',
9215
+ filters: { entityType, entityId, since: since?.toISOString() },
9216
+ }, null, 2) + '\n');
9217
+ return;
9218
+ }
9219
+ if (outputFile) {
9220
+ console.log(`${okLabel()} Exported ${rows.length} ${rows.length === 1 ? 'fact' : 'facts'} → ${outputFile}`);
9221
+ }
9222
+ else {
9223
+ // Stats go to stderr when output is stdout so they don't corrupt the JSONL
9224
+ process.stderr.write(`${okLabel()} Exported ${rows.length} ${rows.length === 1 ? 'fact' : 'facts'}\n`);
9225
+ }
9226
+ }
9227
+ finally {
9228
+ await (0, client_1.disconnectDb)().catch(() => undefined);
9229
+ }
9230
+ }
9231
+ /** Parse repeatable --remap flags into a normalised list. */
9232
+ function parseRemapFlags(args) {
9233
+ const raw = getFlag(args, 'remap');
9234
+ if (!raw)
9235
+ return [];
9236
+ // --remap can appear multiple times; fall back to treating single value as array
9237
+ const values = Array.isArray(raw) ? raw : [raw];
9238
+ return values.flatMap((r) => {
9239
+ const eqIdx = r.indexOf('=');
9240
+ if (eqIdx < 1)
9241
+ return [];
9242
+ const fromStr = r.slice(0, eqIdx).trim();
9243
+ const toStr = r.slice(eqIdx + 1).trim();
9244
+ if (!fromStr || !toStr)
9245
+ return [];
9246
+ const [fromType, fromId] = fromStr.includes('/') ? fromStr.split('/') : [fromStr, undefined];
9247
+ const [toType, toId] = toStr.includes('/') ? toStr.split('/') : [toStr, undefined];
9248
+ return [{ fromType, fromId, toType, toId }];
9249
+ });
9250
+ }
9251
+ async function importCommand(args) {
9252
+ try {
9253
+ const json = hasFlag(args, 'json');
9254
+ const dryRun = hasFlag(args, 'dry-run');
9255
+ const provenanceTag = getFlag(args, 'provenance') ?? undefined;
9256
+ const conflictRaw = getFlag(args, 'conflict') ?? 'skip';
9257
+ const conflictMode = conflictRaw === 'overwrite' ? 'overwrite'
9258
+ : conflictRaw === 'merge' ? 'merge'
9259
+ : 'skip';
9260
+ const remaps = parseRemapFlags(args);
9261
+ const inputArg = args.positionals[0]?.trim();
9262
+ if (!inputArg) {
9263
+ throw cliError('IRANTI_IMPORT_FILE_REQUIRED', 'Missing input file. Usage: iranti import <file>', ['Use "-" to read from stdin.', 'Run `iranti import --help` for options.']);
9264
+ }
9265
+ let rawContent;
9266
+ if (inputArg === '-') {
9267
+ // Read from stdin
9268
+ const chunks = [];
9269
+ for await (const chunk of process.stdin) {
9270
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
9271
+ }
9272
+ rawContent = Buffer.concat(chunks).toString('utf8');
9273
+ }
9274
+ else {
9275
+ const resolved = path_1.default.resolve(inputArg);
9276
+ if (!fs_1.default.existsSync(resolved)) {
9277
+ throw cliError('IRANTI_IMPORT_FILE_NOT_FOUND', `File not found: ${resolved}`, ['Check the path and try again.']);
9278
+ }
9279
+ rawContent = fs_1.default.readFileSync(resolved, 'utf8');
9280
+ }
9281
+ await resolveDbForExportCommands(args);
9282
+ const lines = rawContent.split('\n');
9283
+ let added = 0, skipped = 0, overwritten = 0, parseErrors = 0, dataLines = 0;
9284
+ const conflictDetails = [];
9285
+ for (const line of lines) {
9286
+ const trimmed = line.trim();
9287
+ if (!trimmed)
9288
+ continue;
9289
+ let parsed;
9290
+ try {
9291
+ parsed = JSON.parse(trimmed);
9292
+ }
9293
+ catch {
9294
+ parseErrors++;
9295
+ continue;
9296
+ }
9297
+ // Skip JSONL header lines
9298
+ if (typeof parsed === 'object' &&
9299
+ parsed !== null &&
9300
+ '_type' in parsed)
9301
+ continue;
9302
+ dataLines++;
9303
+ let row = parsed;
9304
+ // Apply namespace remaps
9305
+ for (const remap of remaps) {
9306
+ const typeMatches = row.entityType === remap.fromType;
9307
+ const idMatches = remap.fromId == null || row.entityId === remap.fromId;
9308
+ if (typeMatches && idMatches) {
9309
+ row = {
9310
+ ...row,
9311
+ entityType: remap.toType,
9312
+ entityId: remap.toId ?? row.entityId,
9313
+ };
9314
+ }
9315
+ }
9316
+ if (!dryRun) {
9317
+ const result = await (0, queries_1.importFact)(row, conflictMode, provenanceTag);
9318
+ if (result.outcome === 'added')
9319
+ added++;
9320
+ else if (result.outcome === 'overwritten')
9321
+ overwritten++;
9322
+ else {
9323
+ skipped++;
9324
+ if (result.reason) {
9325
+ conflictDetails.push({ entityType: row.entityType, entityId: row.entityId, key: row.key, reason: result.reason });
9326
+ }
9327
+ }
9328
+ }
9329
+ else {
9330
+ skipped++;
9331
+ }
9332
+ }
9333
+ if (json) {
9334
+ console.log(JSON.stringify({
9335
+ ok: true,
9336
+ dryRun,
9337
+ conflict: conflictMode,
9338
+ total: dataLines,
9339
+ added,
9340
+ skipped,
9341
+ overwritten,
9342
+ parseErrors,
9343
+ ...(conflictDetails.length > 0 ? { conflicts: conflictDetails } : {}),
9344
+ }, null, 2));
9345
+ process.exit(0);
9346
+ }
9347
+ const mode = dryRun ? ' (dry-run)' : '';
9348
+ console.log(sectionTitle(`Import results${mode}`));
9349
+ console.log(` conflict: ${conflictMode}`);
9350
+ console.log(` total: ${dataLines}`);
9351
+ if (!dryRun) {
9352
+ console.log(` added: ${added}`);
9353
+ console.log(` overwritten: ${overwritten}`);
9354
+ console.log(` skipped: ${skipped}`);
9355
+ }
9356
+ else {
9357
+ console.log(` (dry-run — no writes performed, ${dataLines} rows parsed)`);
9358
+ }
9359
+ if (parseErrors > 0) {
9360
+ console.log(` parse errors: ${parseErrors}`);
9361
+ }
9362
+ if (!dryRun) {
9363
+ console.log(`\n${okLabel()} Import complete.`);
9364
+ }
9365
+ }
9366
+ finally {
9367
+ await (0, client_1.disconnectDb)().catch(() => undefined);
9368
+ }
9369
+ }
9370
+ async function snapshotCommand(args) {
9371
+ const subcommand = args.subcommand;
9372
+ const cwd = path_1.default.resolve(getFlag(args, 'project') ?? process.cwd());
9373
+ const snapshotDir = path_1.default.join(cwd, SNAPSHOT_DIR);
9374
+ if (!subcommand || subcommand === 'help' || subcommand === '--help') {
9375
+ printSnapshotHelp();
9376
+ return;
9377
+ }
9378
+ if (subcommand === 'list') {
9379
+ if (!fs_1.default.existsSync(snapshotDir)) {
9380
+ console.log(`${infoLabel()} No snapshots found. Create one with: iranti snapshot create <name>`);
9381
+ return;
9382
+ }
9383
+ const files = fs_1.default.readdirSync(snapshotDir).filter((f) => f.endsWith('.jsonl')).sort();
9384
+ if (files.length === 0) {
9385
+ console.log(`${infoLabel()} No snapshots found. Create one with: iranti snapshot create <name>`);
9386
+ return;
9387
+ }
9388
+ console.log(sectionTitle(`Snapshots (${files.length})`));
9389
+ for (const file of files) {
9390
+ const fullPath = path_1.default.join(snapshotDir, file);
9391
+ const stat = fs_1.default.statSync(fullPath);
9392
+ const sizeKb = (stat.size / 1024).toFixed(1);
9393
+ console.log(` ${commandText(file.replace('.jsonl', ''))} ${sizeKb} KB ${stat.mtime.toISOString()}`);
9394
+ }
9395
+ return;
9396
+ }
9397
+ if (subcommand === 'create') {
9398
+ const name = args.positionals[0]?.trim();
9399
+ if (!name) {
9400
+ throw cliError('IRANTI_SNAPSHOT_NAME_REQUIRED', 'Missing snapshot name. Usage: iranti snapshot create <name>', ['Example: iranti snapshot create pre-refactor']);
9401
+ }
9402
+ const safeName = name.replace(/[^a-zA-Z0-9._-]/g, '_');
9403
+ const snapshotPath = path_1.default.join(snapshotDir, `${safeName}.jsonl`);
9404
+ try {
9405
+ await resolveDbForExportCommands(args);
9406
+ const rows = await (0, queries_1.exportFacts)({});
9407
+ const exportedAt = new Date().toISOString();
9408
+ const header = { _type: 'iranti-export', version: '1', exportedAt, snapshotName: safeName, total: rows.length };
9409
+ const lines = [JSON.stringify(header), ...rows.map((r) => JSON.stringify(r))];
9410
+ fs_1.default.mkdirSync(snapshotDir, { recursive: true });
9411
+ fs_1.default.writeFileSync(snapshotPath, lines.join('\n') + '\n', 'utf8');
9412
+ console.log(`${okLabel()} Snapshot "${safeName}" created → ${snapshotPath} (${rows.length} facts)`);
9413
+ }
9414
+ finally {
9415
+ await (0, client_1.disconnectDb)().catch(() => undefined);
9416
+ }
9417
+ return;
9418
+ }
9419
+ if (subcommand === 'restore') {
9420
+ const name = args.positionals[0]?.trim();
9421
+ if (!name) {
9422
+ throw cliError('IRANTI_SNAPSHOT_NAME_REQUIRED', 'Missing snapshot name. Usage: iranti snapshot restore <name>', ['Run `iranti snapshot list` to see available snapshots.']);
9423
+ }
9424
+ const safeName = name.replace(/[^a-zA-Z0-9._-]/g, '_');
9425
+ const snapshotPath = path_1.default.join(snapshotDir, `${safeName}.jsonl`);
9426
+ if (!fs_1.default.existsSync(snapshotPath)) {
9427
+ throw cliError('IRANTI_SNAPSHOT_NOT_FOUND', `Snapshot "${safeName}" not found at ${snapshotPath}.`, ['Run `iranti snapshot list` to see available snapshots.']);
9428
+ }
9429
+ const conflictRaw = getFlag(args, 'conflict') ?? 'overwrite';
9430
+ const conflictMode = conflictRaw === 'skip' ? 'skip'
9431
+ : conflictRaw === 'merge' ? 'merge'
9432
+ : 'overwrite';
9433
+ const dryRun = hasFlag(args, 'dry-run');
9434
+ const json = hasFlag(args, 'json');
9435
+ const rawContent = fs_1.default.readFileSync(snapshotPath, 'utf8');
9436
+ const lines = rawContent.split('\n');
9437
+ try {
9438
+ await resolveDbForExportCommands(args);
9439
+ let added = 0, skipped = 0, overwritten = 0, parseErrors = 0, dataLines = 0;
9440
+ for (const line of lines) {
9441
+ const trimmed = line.trim();
9442
+ if (!trimmed)
9443
+ continue;
9444
+ let parsed;
9445
+ try {
9446
+ parsed = JSON.parse(trimmed);
9447
+ }
9448
+ catch {
9449
+ parseErrors++;
9450
+ continue;
9451
+ }
9452
+ if (typeof parsed === 'object' &&
9453
+ parsed !== null &&
9454
+ '_type' in parsed)
9455
+ continue;
9456
+ dataLines++;
9457
+ const row = parsed;
9458
+ if (!dryRun) {
9459
+ const result = await (0, queries_1.importFact)(row, conflictMode, `snapshot:${safeName}`);
9460
+ if (result.outcome === 'added')
9461
+ added++;
9462
+ else if (result.outcome === 'overwritten')
9463
+ overwritten++;
9464
+ else
9465
+ skipped++;
9466
+ }
9467
+ else {
9468
+ skipped++;
9469
+ }
9470
+ }
9471
+ if (json) {
9472
+ console.log(JSON.stringify({ ok: true, snapshot: safeName, dryRun, conflict: conflictMode, total: dataLines, added, skipped, overwritten, parseErrors }, null, 2));
9473
+ process.exit(0);
9474
+ }
9475
+ const mode = dryRun ? ' (dry-run)' : '';
9476
+ console.log(sectionTitle(`Snapshot restore: "${safeName}"${mode}`));
9477
+ console.log(` conflict: ${conflictMode}`);
9478
+ console.log(` total: ${dataLines}`);
9479
+ if (!dryRun) {
9480
+ console.log(` added: ${added}`);
9481
+ console.log(` overwritten: ${overwritten}`);
9482
+ console.log(` skipped: ${skipped}`);
9483
+ console.log(`\n${okLabel()} Restore complete.`);
9484
+ }
9485
+ else {
9486
+ console.log(` (dry-run — no writes performed)`);
9487
+ }
9488
+ }
9489
+ finally {
9490
+ await (0, client_1.disconnectDb)().catch(() => undefined);
9491
+ }
9492
+ return;
9493
+ }
9494
+ if (subcommand === 'delete') {
9495
+ const name = args.positionals[0]?.trim();
9496
+ if (!name) {
9497
+ throw cliError('IRANTI_SNAPSHOT_NAME_REQUIRED', 'Missing snapshot name. Usage: iranti snapshot delete <name>', ['Run `iranti snapshot list` to see available snapshots.']);
9498
+ }
9499
+ const safeName = name.replace(/[^a-zA-Z0-9._-]/g, '_');
9500
+ const snapshotPath = path_1.default.join(snapshotDir, `${safeName}.jsonl`);
9501
+ if (!fs_1.default.existsSync(snapshotPath)) {
9502
+ throw cliError('IRANTI_SNAPSHOT_NOT_FOUND', `Snapshot "${safeName}" not found at ${snapshotPath}.`, ['Run `iranti snapshot list` to see available snapshots.']);
9503
+ }
9504
+ fs_1.default.unlinkSync(snapshotPath);
9505
+ console.log(`${okLabel()} Snapshot "${safeName}" deleted.`);
9506
+ return;
9507
+ }
9508
+ throw cliError('IRANTI_SNAPSHOT_UNKNOWN_SUBCOMMAND', `Unknown snapshot subcommand: "${subcommand}". Use: create, restore, list, delete.`, ['Run `iranti snapshot --help` to see usage.']);
9509
+ }
9008
9510
  /**
9009
9511
  * A2: revert-autowrite CLI — pair for M2 tool-result extraction. Every
9010
9512
  * attendant_autowrite stamps the knowledge entry with source=attendant_autowrite
@@ -9130,6 +9632,34 @@ async function revertAutowriteCommand(args) {
9130
9632
  await (0, client_1.disconnectDb)().catch(() => undefined);
9131
9633
  }
9132
9634
  }
9635
+ /**
9636
+ * `iranti feedback` — collect a one-keypress satisfaction signal.
9637
+ *
9638
+ * No DB required. Reads .iranti/session-stats.json for passive usage context,
9639
+ * maintains a throttle record in .iranti/feedback-sent.json, and falls back
9640
+ * to .iranti/pending-feedback.json when the endpoint is unreachable.
9641
+ */
9642
+ async function feedbackCommand(args) {
9643
+ const ratingStr = getFlag(args, 'rating');
9644
+ const rating = ratingStr !== undefined ? Number(ratingStr) : undefined;
9645
+ const comment = getFlag(args, 'comment');
9646
+ const typeStr = getFlag(args, 'type') ?? 'general';
9647
+ const dryRun = hasFlag(args, 'dry-run');
9648
+ const offline = hasFlag(args, 'offline');
9649
+ const milestoneContext = getFlag(args, 'milestone') ?? null;
9650
+ const type = typeStr;
9651
+ const opts = {
9652
+ rating,
9653
+ comment: comment ?? undefined,
9654
+ type,
9655
+ dryRun,
9656
+ offline,
9657
+ milestoneContext,
9658
+ factCount: null,
9659
+ version: getPackageVersion(),
9660
+ };
9661
+ await (0, feedbackCollector_1.runFeedbackCommand)(opts);
9662
+ }
9133
9663
  async function main() {
9134
9664
  const args = parseArgs(process.argv.slice(2));
9135
9665
  ACTIVE_PARSED_ARGS = args;
@@ -9432,6 +9962,30 @@ async function main() {
9432
9962
  await deleteRuleCommand(args);
9433
9963
  return;
9434
9964
  }
9965
+ if (args.command === 'export') {
9966
+ if (hasFlag(args, 'help')) {
9967
+ printExportHelp();
9968
+ return;
9969
+ }
9970
+ await exportCommand(args);
9971
+ return;
9972
+ }
9973
+ if (args.command === 'import') {
9974
+ if (hasFlag(args, 'help')) {
9975
+ printImportHelp();
9976
+ return;
9977
+ }
9978
+ await importCommand(args);
9979
+ return;
9980
+ }
9981
+ if (args.command === 'snapshot') {
9982
+ if (!args.subcommand || args.subcommand === 'help' || args.subcommand === '--help' || hasFlag(args, 'help')) {
9983
+ printSnapshotHelp();
9984
+ return;
9985
+ }
9986
+ await snapshotCommand(args);
9987
+ return;
9988
+ }
9435
9989
  if (args.command === 'revert-autowrite') {
9436
9990
  if (hasFlag(args, 'help')) {
9437
9991
  printRevertAutowriteHelp();
@@ -9440,6 +9994,14 @@ async function main() {
9440
9994
  await revertAutowriteCommand(args);
9441
9995
  return;
9442
9996
  }
9997
+ if (args.command === 'feedback' || args.command === 'fb') {
9998
+ if (hasFlag(args, 'help')) {
9999
+ printFeedbackHelp();
10000
+ return;
10001
+ }
10002
+ await feedbackCommand(args);
10003
+ return;
10004
+ }
9443
10005
  if (args.command === 'claude-setup') {
9444
10006
  await claudeSetupCommand(args);
9445
10007
  return;