enigma-memory 0.1.0 → 0.1.2

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.
@@ -0,0 +1,289 @@
1
+ const NATIVE_HOST = 'com.enigma.native_host';
2
+ const PROTOCOL = 'enigma.native.browser.v1';
3
+ const REQUEST_TIMEOUT_MS = 30000;
4
+ const MAX_CONTEXT_CHARS = 32000;
5
+ const SAFE_NATIVE_ERROR_CODE = /^[A-Z][A-Z0-9_]{0,31}$/;
6
+
7
+ export const SUPPORTED_PROVIDERS = Object.freeze({
8
+ chatgpt: Object.freeze({ id: 'chatgpt', label: 'ChatGPT', hosts: Object.freeze(['chatgpt.com', 'chat.openai.com']) }),
9
+ claude: Object.freeze({ id: 'claude', label: 'Claude', hosts: Object.freeze(['claude.ai']) }),
10
+ kimi: Object.freeze({ id: 'kimi', label: 'Kimi', hosts: Object.freeze(['kimi.com', 'www.kimi.com', 'kimi.moonshot.cn']) }),
11
+ perplexity: Object.freeze({ id: 'perplexity', label: 'Perplexity', hosts: Object.freeze(['perplexity.ai', 'www.perplexity.ai']) })
12
+ });
13
+
14
+ const RECEIPT_RAW_FIELD_NAMES = new Set([
15
+ 'memory',
16
+ 'memories',
17
+ 'plaintext',
18
+ 'plainText',
19
+ 'raw',
20
+ 'rawMemory',
21
+ 'rawMemories',
22
+ 'context',
23
+ 'contextText',
24
+ 'text',
25
+ 'content',
26
+ 'prompt'
27
+ ]);
28
+
29
+ export function detectProviderFromUrl(url) {
30
+ let parsed;
31
+ try {
32
+ parsed = new URL(String(url));
33
+ } catch {
34
+ return undefined;
35
+ }
36
+
37
+ if (parsed.protocol !== 'https:') return undefined;
38
+ const hostname = parsed.hostname.toLowerCase();
39
+ for (const provider of Object.values(SUPPORTED_PROVIDERS)) {
40
+ if (provider.hosts.includes(hostname)) return provider;
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ export async function requestContextPack(input) {
46
+ const provider = requireSupportedProvider(input?.url, input?.providerId);
47
+ const response = await sendNativeMessage({
48
+ protocol: PROTOCOL,
49
+ id: requestId(),
50
+ type: 'enigma.browser.context.request',
51
+ provider: provider.id,
52
+ page: sanitizePage(input, provider),
53
+ selection: sanitizeSelection(input?.selection),
54
+ requirements: {
55
+ custody: 'local-only',
56
+ approval: 'user-click',
57
+ receipt: 'required',
58
+ receiptPlaintext: 'forbidden',
59
+ providerNativeMemory: 'cache-only',
60
+ storage: 'transient-extension-memory'
61
+ }
62
+ });
63
+
64
+ return validateContextResponse(response);
65
+ }
66
+
67
+ export async function recordInsertionReceipt(input) {
68
+ const provider = requireSupportedProvider(input?.url, input?.providerId);
69
+ const receipt = sanitizeReceipt(input?.receipt);
70
+ const response = await sendNativeMessage({
71
+ protocol: PROTOCOL,
72
+ id: requestId(),
73
+ type: 'enigma.browser.insertion.record',
74
+ provider: provider.id,
75
+ page: sanitizePage(input, provider),
76
+ insertion: {
77
+ mode: input?.mode === 'replace-selection' ? 'replace-selection' : 'insert-at-cursor',
78
+ target: typeof input?.target === 'string' ? input.target.slice(0, 80) : 'prompt',
79
+ insertedCharCount: nonNegativeInteger(input?.insertedCharCount),
80
+ insertedAt: requireString(input?.insertedAt, 'insertion.insertedAt', 80),
81
+ receipt
82
+ },
83
+ requirements: {
84
+ plaintextInRecord: 'forbidden',
85
+ receiptPlaintext: 'forbidden'
86
+ }
87
+ });
88
+
89
+ if (!response || response.protocol !== PROTOCOL || response.type !== 'enigma.browser.insertion.recorded') {
90
+ throw new Error('Native host returned an invalid insertion receipt acknowledgement.');
91
+ }
92
+ if (response.ok !== true) {
93
+ throw new Error(nativeHostErrorMessage('Native host rejected insertion receipt.', response));
94
+ }
95
+ return Object.freeze({ ok: true, receipt });
96
+ }
97
+
98
+ export function serializeError(error) {
99
+ if (error instanceof Error) return { message: sanitizeSerializableErrorMessage(error.message) };
100
+ return { message: 'Unexpected Enigma bridge error.' };
101
+ }
102
+
103
+ function requireSupportedProvider(url, expectedProviderId) {
104
+ const provider = detectProviderFromUrl(url);
105
+ if (!provider) throw new Error('Unsupported or unknown AI provider page.');
106
+ if (expectedProviderId !== undefined && expectedProviderId !== provider.id) {
107
+ throw new Error('Provider mismatch for active tab URL.');
108
+ }
109
+ return provider;
110
+ }
111
+
112
+ function sanitizePage(input, provider) {
113
+ const parsed = new URL(String(input?.url ?? ''));
114
+ return Object.freeze({
115
+ providerId: provider.id,
116
+ origin: parsed.origin,
117
+ hostname: parsed.hostname.toLowerCase(),
118
+ display: Object.freeze({
119
+ providerLabel: provider.label,
120
+ hostLabel: parsed.hostname.toLowerCase(),
121
+ titlePresent: typeof input?.title === 'string' && input.title.length > 0,
122
+ titleCharCount: typeof input?.title === 'string' ? input.title.length : 0
123
+ }),
124
+ topLevel: input?.topLevel !== false
125
+ });
126
+ }
127
+
128
+ function sanitizeSelection(selection) {
129
+ if (!selection || typeof selection !== 'object') return undefined;
130
+ const text = typeof selection.text === 'string' ? selection.text : '';
131
+ if (text.length === 0) return undefined;
132
+ if (text.length > 4000) throw new Error('Selected page text exceeds 4000 characters.');
133
+ const source = typeof selection.source === 'string' && selection.source.length > 0 ? selection.source : 'user-selection';
134
+ if (source.length > 80) throw new Error('selection.source exceeds 80 characters.');
135
+ return Object.freeze({
136
+ text,
137
+ source
138
+ });
139
+ }
140
+
141
+ function validateContextResponse(response) {
142
+ if (!response || response.protocol !== PROTOCOL || response.type !== 'enigma.browser.context.response') {
143
+ throw new Error('Native host returned an invalid Enigma context response.');
144
+ }
145
+ if (response.ok !== true) {
146
+ throw new Error(nativeHostErrorMessage('Native host rejected context request.', response));
147
+ }
148
+
149
+ const text = response.context?.text;
150
+ if (typeof text !== 'string' || text.length === 0) {
151
+ throw new Error('Native host did not return insertable context.');
152
+ }
153
+ if (text.length > MAX_CONTEXT_CHARS) {
154
+ throw new Error(`Native host context exceeds ${MAX_CONTEXT_CHARS} characters.`);
155
+ }
156
+
157
+ const receipt = sanitizeReceipt(response.receipt);
158
+ return Object.freeze({
159
+ ok: true,
160
+ context: Object.freeze({
161
+ text,
162
+ mime: response.context?.mime === 'text/markdown' ? 'text/markdown' : 'text/plain',
163
+ charCount: text.length
164
+ }),
165
+ receipt
166
+ });
167
+ }
168
+
169
+ function sanitizeReceipt(receipt) {
170
+ if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)) {
171
+ throw new Error('Enigma native host must return a receipt object.');
172
+ }
173
+ if (containsRawReceiptField(receipt)) {
174
+ throw new Error('Receipt contains a forbidden plaintext-like field.');
175
+ }
176
+
177
+ const id = requireString(receipt.id, 'receipt.id', 160);
178
+ const commitment = requireString(receipt.commitment ?? receipt.digest, 'receipt.commitment', 256);
179
+ const digestAlgorithm = requireString(receipt.digestAlgorithm, 'receipt.digestAlgorithm', 80);
180
+ const createdAt = requireString(receipt.createdAt, 'receipt.createdAt', 80);
181
+
182
+ return Object.freeze({ id, commitment, digestAlgorithm, createdAt });
183
+ }
184
+
185
+ function containsRawReceiptField(value, depth = 0) {
186
+ if (depth > 12 || value === null || typeof value !== 'object') return false;
187
+ if (Array.isArray(value)) return value.some((item) => containsRawReceiptField(item, depth + 1));
188
+ for (const [key, child] of Object.entries(value)) {
189
+ if (RECEIPT_RAW_FIELD_NAMES.has(key)) return true;
190
+ if (containsRawReceiptField(child, depth + 1)) return true;
191
+ }
192
+ return false;
193
+ }
194
+
195
+ function requireString(value, name, maxLength) {
196
+ if (typeof value !== 'string' || value.length === 0) throw new Error(`Missing ${name}.`);
197
+ if (value.length > maxLength) throw new Error(`${name} exceeds ${maxLength} characters.`);
198
+ return value;
199
+ }
200
+
201
+ function nonNegativeInteger(value) {
202
+ return Number.isSafeInteger(value) && value >= 0 ? value : 0;
203
+ }
204
+
205
+ function requestId() {
206
+ const random = new Uint8Array(16);
207
+ crypto.getRandomValues(random);
208
+ return Array.from(random, (byte) => byte.toString(16).padStart(2, '0')).join('');
209
+ }
210
+
211
+ function sendNativeMessage(message) {
212
+ return new Promise((resolve, reject) => {
213
+ let settled = false;
214
+ const timeout = setTimeout(() => {
215
+ if (settled) return;
216
+ settled = true;
217
+ reject(new Error('Timed out waiting for Enigma native host.'));
218
+ }, REQUEST_TIMEOUT_MS);
219
+
220
+ chrome.runtime.sendNativeMessage(NATIVE_HOST, message, (response) => {
221
+ if (settled) return;
222
+ settled = true;
223
+ clearTimeout(timeout);
224
+
225
+ const lastError = chrome.runtime.lastError;
226
+ if (lastError) {
227
+ reject(new Error(nativeHostErrorMessage('Unable to reach Enigma native host.', lastError)));
228
+ return;
229
+ }
230
+ if (response && response.id !== message.id) {
231
+ reject(new Error('Native host response id did not match the request id.'));
232
+ return;
233
+ }
234
+ resolve(response);
235
+ });
236
+ });
237
+ }
238
+
239
+ function nativeHostErrorMessage(fallback, detail) {
240
+ const code = safeNativeErrorCode(detail);
241
+ return code ? `${fallback} (${code})` : fallback;
242
+ }
243
+
244
+ function safeNativeErrorCode(detail) {
245
+ if (!detail || typeof detail !== 'object') return undefined;
246
+ const code = detail.code ?? detail.errorCode;
247
+ if (typeof code !== 'string') return undefined;
248
+ const trimmed = code.trim();
249
+ return SAFE_NATIVE_ERROR_CODE.test(trimmed) ? trimmed : undefined;
250
+ }
251
+
252
+ function sanitizeSerializableErrorMessage(message) {
253
+ if (typeof message !== 'string') return 'Unexpected Enigma bridge error.';
254
+ const trimmed = message.trim();
255
+ if (!trimmed) return 'Unexpected Enigma bridge error.';
256
+ if (trimmed.length > 240 || looksLikeSensitiveErrorText(trimmed) || !isKnownSafeErrorMessage(trimmed)) {
257
+ return 'Enigma action failed without exposing local memory.';
258
+ }
259
+ return trimmed;
260
+ }
261
+
262
+ function isKnownSafeErrorMessage(message) {
263
+ switch (message) {
264
+ case 'Unsupported or unknown AI provider page.':
265
+ case 'Provider mismatch for active tab URL.':
266
+ case 'Message provider does not match the active page.':
267
+ case 'Unsupported Enigma extension message.':
268
+ case 'Native host rejected context request.':
269
+ case 'Native host rejected insertion receipt.':
270
+ case 'Unable to reach Enigma native host.':
271
+ case 'Timed out waiting for Enigma native host.':
272
+ case 'Native host returned an invalid Enigma context response.':
273
+ case 'Native host returned an invalid insertion receipt acknowledgement.':
274
+ case 'Native host response id did not match the request id.':
275
+ case 'Native host did not return insertable context.':
276
+ case 'Enigma native host must return a receipt object.':
277
+ case 'Receipt contains a forbidden plaintext-like field.':
278
+ case 'Selected page text exceeds 4000 characters.':
279
+ case 'selection.source exceeds 80 characters.':
280
+ return true;
281
+ default:
282
+ return /^(?:Native host context exceeds|Missing [A-Za-z0-9_.]+|[A-Za-z0-9_.]+ exceeds) \d+ characters\.$/.test(message) ||
283
+ /^Missing [A-Za-z0-9_.]+\.$/.test(message);
284
+ }
285
+ }
286
+
287
+ function looksLikeSensitiveErrorText(text) {
288
+ return /https?:\/\/|[?&#](?:token|key|prompt|q)=|\b(selectedText|selected_text|plaintext|rawMemory|raw_memory|contextText|context_text)\b|["'](?:text|content|memory|raw|prompt|receipt)["']\s*:/i.test(text);
289
+ }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { createHash } from 'node:crypto';
2
3
  import { createServer as createHttpServer } from 'node:http';
3
4
  import { realpathSync } from 'node:fs';
4
5
  import { access, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
@@ -29,6 +30,25 @@ import {
29
30
  const DEFAULT_BUNDLE = '.enigma/bundle.json';
30
31
  export const DEFAULT_RELAY_PORT = 8787;
31
32
  export const DEFAULT_GATEWAY_PORT = 8797;
33
+ const DEFAULT_QUICKSTART_MEMORY = 'Enigma quickstart demo memory: local proof bundles can be created and verified without provider or cloud credentials.';
34
+ const DEFAULT_CROSS_MODEL_DEMO_BUNDLE = '.enigma/cross-model-demo-bundle.json';
35
+ const DEFAULT_CROSS_MODEL_MEMORY = 'Enigma cross-model demo memory: a local encrypted memory can be packaged for ChatGPT, Claude, Kimi, Cursor, and a local LLM without provider credentials.';
36
+ const CROSS_MODEL_PROFILES = Object.freeze([
37
+ { id: 'chatgpt', provider: 'chatgpt', model: 'chatgpt-mcp-profile', label: 'ChatGPT' },
38
+ { id: 'claude', provider: 'claude', model: 'claude-mcp-profile', label: 'Claude' },
39
+ { id: 'kimi', provider: 'kimi', model: 'kimi-mcp-profile', label: 'Kimi' },
40
+ { id: 'cursor', provider: 'cursor', model: 'cursor-mcp-profile', label: 'Cursor' },
41
+ { id: 'local-llm', provider: 'local', model: 'local-llm-profile', label: 'Local LLM' },
42
+ ]);
43
+ const CROSS_MODEL_CLAIM_BOUNDARIES = Object.freeze({
44
+ local_only: true,
45
+ provider_credentials_required: false,
46
+ provider_native_memory_canonical: false,
47
+ provider_deletion_proof: false,
48
+ model_forgetting_proof: false,
49
+ roi_or_savings_guarantee: false,
50
+ compliance_certification: false,
51
+ });
32
52
  const PACKAGE_JSON_URL = new URL('../../../package.json', import.meta.url);
33
53
  const SPECS_URL = new URL('../../../specs/', import.meta.url);
34
54
  const IMPORTERS = Object.freeze({
@@ -145,6 +165,118 @@ async function fileExists(path) {
145
165
  }
146
166
  }
147
167
 
168
+ function pathFlag(flags, names, fallback) {
169
+ const value = getFlag(flags, names, fallback);
170
+ if (value === true || value === '') throw new Error(`Missing required --${names[0]}.`);
171
+ return String(value);
172
+ }
173
+
174
+ function quickstartPathDisplay(outDirInput, name) {
175
+ const base = String(outDirInput);
176
+ if (base === '' || base === '.') return name;
177
+ return `${base.replace(/[\\/]+$/, '')}/${name}`;
178
+ }
179
+
180
+ function ensureDistinctOutputPaths(paths) {
181
+ const normalized = paths.map((path) => (process.platform === 'win32' ? path.toLowerCase() : path));
182
+ if (new Set(normalized).size !== paths.length) {
183
+ throw new Error('Quickstart output paths must be distinct.');
184
+ }
185
+ }
186
+
187
+ async function assertCanWriteQuickstartOutputs(outputs, overwrite) {
188
+ if (overwrite) return;
189
+ const existing = [];
190
+ for (const output of outputs) {
191
+ if (await fileExists(output.path)) existing.push(output.display);
192
+ }
193
+ if (existing.length > 0) {
194
+ throw new Error(`Quickstart output already exists: ${existing.join(', ')}. Pass --overwrite to replace it.`);
195
+ }
196
+ }
197
+
198
+ async function quickstartMemoryTextFromFlags(flags) {
199
+ const inlineText = getFlag(flags, ['memory-text', 'memoryText']);
200
+ const textFile = getFlag(flags, ['memory-file', 'memoryFile', 'text-file', 'textFile']);
201
+ if (inlineText !== undefined && textFile !== undefined) throw new Error('Use either --memory-text or --memory-file, not both.');
202
+ if (inlineText !== undefined) {
203
+ if (inlineText === true || inlineText === '') throw new Error('Missing required --memory-text.');
204
+ return String(inlineText);
205
+ }
206
+ if (textFile !== undefined) {
207
+ if (textFile === true || textFile === '') throw new Error('Missing required --memory-file.');
208
+ return readFile(resolve(String(textFile)), 'utf8');
209
+ }
210
+ return DEFAULT_QUICKSTART_MEMORY;
211
+ }
212
+
213
+ async function crossModelMemoryTextFromFlags(flags) {
214
+ const textFile = getFlag(flags, ['memory-file', 'memoryFile', 'text-file', 'textFile']);
215
+ if (textFile === undefined) return DEFAULT_CROSS_MODEL_MEMORY;
216
+ if (textFile === true || textFile === '') throw new Error('Missing required --memory-file.');
217
+ return readFile(resolve(String(textFile)), 'utf8');
218
+ }
219
+
220
+ function activeMemoryCount(vault) {
221
+ return vault.activeAddresses instanceof Set ? vault.activeAddresses.size : 0;
222
+ }
223
+
224
+ function sha256Json(value) {
225
+ return `sha256:${createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
226
+ }
227
+
228
+ function contextPackPublicDigest(pack) {
229
+ return sha256Json({
230
+ schema: pack.schema,
231
+ context_pack_id: pack.context_pack_id,
232
+ provider: pack.provider,
233
+ model: pack.model,
234
+ purpose: pack.purpose,
235
+ memory_addresses: pack.memory_addresses,
236
+ receipt_hashes: pack.receipt_hashes,
237
+ active_set_root: pack.active_set_root,
238
+ receipt_log_root: pack.receipt_log_root,
239
+ });
240
+ }
241
+
242
+ function publicReceiptRefs(receipts) {
243
+ return (Array.isArray(receipts) ? receipts : []).map((receipt) => ({
244
+ receipt_id: receipt.receipt_id,
245
+ operation: receipt.operation,
246
+ memory_addr: receipt.memory_addr,
247
+ provider: receipt.provider,
248
+ model: receipt.model,
249
+ event_hash: receipt.event_hash,
250
+ receipt_log_root: receipt.receipt_log_root,
251
+ timestamp: receipt.timestamp,
252
+ }));
253
+ }
254
+
255
+ function publicContextPackSummary(pack) {
256
+ const receipts = publicReceiptRefs(pack.receipts);
257
+ return {
258
+ schema: pack.schema,
259
+ context_pack_ref: `enigma://context-pack/${pack.context_pack_id}`,
260
+ context_pack_id: pack.context_pack_id,
261
+ context_pack_digest: contextPackPublicDigest(pack),
262
+ provider: pack.provider,
263
+ model: pack.model,
264
+ purpose: pack.purpose,
265
+ memory_addresses: Array.isArray(pack.memory_addresses) ? [...pack.memory_addresses] : [],
266
+ memory_count: Array.isArray(pack.memory_addresses) ? pack.memory_addresses.length : 0,
267
+ receipt_count: receipts.length,
268
+ receipt_hashes: Array.isArray(pack.receipt_hashes) ? [...pack.receipt_hashes] : [],
269
+ receipts,
270
+ active_set_root: pack.active_set_root,
271
+ receipt_log_root: pack.receipt_log_root,
272
+ content_redacted: true,
273
+ };
274
+ }
275
+
276
+ function demoBundleRef(bundleWasSupplied) {
277
+ return bundleWasSupplied ? 'supplied_bundle' : DEFAULT_CROSS_MODEL_DEMO_BUNDLE;
278
+ }
279
+
148
280
  async function readPackageJson() {
149
281
  return readJson(PACKAGE_JSON_URL);
150
282
  }
@@ -361,6 +493,200 @@ async function initCommand(flags, io) {
361
493
  return 0;
362
494
  }
363
495
 
496
+ export async function quickstartCommand(flags, io) {
497
+ const bundleInput = pathFlag(flags, ['bundle', 'file'], DEFAULT_BUNDLE);
498
+ const outDirInput = pathFlag(flags, ['out-dir', 'outDir'], dirname(bundleInput));
499
+ const bundlePath = resolve(bundleInput);
500
+ const outDirPath = resolve(outDirInput);
501
+ const contextPackPath = resolve(outDirPath, 'context-pack.json');
502
+ const exportPath = resolve(outDirPath, 'export.json');
503
+ const verifyReportPath = resolve(outDirPath, 'verify-report.json');
504
+ const contextPackDisplay = quickstartPathDisplay(outDirInput, 'context-pack.json');
505
+ const exportDisplay = quickstartPathDisplay(outDirInput, 'export.json');
506
+ const verifyReportDisplay = quickstartPathDisplay(outDirInput, 'verify-report.json');
507
+ const outputs = [
508
+ { path: bundlePath, display: bundleInput },
509
+ { path: contextPackPath, display: contextPackDisplay },
510
+ { path: exportPath, display: exportDisplay },
511
+ { path: verifyReportPath, display: verifyReportDisplay },
512
+ ];
513
+ ensureDistinctOutputPaths(outputs.map((output) => output.path));
514
+ const overwrite = getFlag(flags, ['overwrite'], false) === true || getFlag(flags, ['overwrite'], false) === 'true';
515
+ await assertCanWriteQuickstartOutputs(outputs, overwrite);
516
+
517
+ const vault = createVault({
518
+ subjectId: String(getFlag(flags, ['subject', 'subject-id'], 'local-user')),
519
+ displayName: String(getFlag(flags, ['display-name', 'name'], 'Local user')),
520
+ passphrase: String(getFlag(flags, ['passphrase'], 'local-development-passphrase')),
521
+ });
522
+ const passport = createPassport({
523
+ vault,
524
+ subjectId: vault.subject_id,
525
+ displayName: String(getFlag(flags, ['display-name', 'name'], 'Local user')),
526
+ });
527
+ remember({
528
+ vault,
529
+ passport,
530
+ text: await quickstartMemoryTextFromFlags(flags),
531
+ purpose: 'quickstart_local_proof',
532
+ purpose_tags: ['quickstart'],
533
+ metadata: { source: 'enigma quickstart' },
534
+ });
535
+ const contextPack = compileContextPack({
536
+ vault,
537
+ passport,
538
+ query: '',
539
+ purpose: 'quickstart_local_context',
540
+ limit: 8,
541
+ });
542
+ const exported = exportBundle({ vault, includePlaintext: false });
543
+ const bundle = exported.bundle ?? exported;
544
+ const verifyReport = verifyBundle(bundle);
545
+
546
+ await writeJson(bundlePath, bundle);
547
+ await writeJson(contextPackPath, contextPack);
548
+ await writeJson(exportPath, bundle);
549
+ await writeJson(verifyReportPath, verifyReport);
550
+
551
+ print({
552
+ ok: verifyReport.ok === true,
553
+ bundle: bundleInput,
554
+ context_pack: contextPackDisplay,
555
+ export: exportDisplay,
556
+ verify_report: verifyReportDisplay,
557
+ memory_count: Array.isArray(bundle.memory_objects) ? bundle.memory_objects.length : 0,
558
+ receipt_count: Array.isArray(bundle.receipts) ? bundle.receipts.length : 0,
559
+ context_item_count: Array.isArray(contextPack.memories) ? contextPack.memories.length : 0,
560
+ verify_ok: verifyReport.ok === true,
561
+ next_commands: [
562
+ `enigma verify --export ${exportDisplay}`,
563
+ `enigma connect generic-mcp --bundle ${bundleInput} --dry-run`,
564
+ ],
565
+ claim_boundaries: {
566
+ local_only: true,
567
+ provider_credentials_required: false,
568
+ provider_deletion_proof: false,
569
+ model_forgetting_proof: false,
570
+ roi_or_savings_guarantee: false,
571
+ compliance_certification: false,
572
+ },
573
+ }, io);
574
+ return verifyReport.ok === true ? 0 : 1;
575
+ }
576
+
577
+ export async function crossModelDemoCommand(flags, io) {
578
+ const bundleFlag = getFlag(flags, ['bundle', 'file']);
579
+ if (bundleFlag === true || bundleFlag === '') throw new Error('Missing required --bundle.');
580
+ const bundleWasSupplied = bundleFlag !== undefined;
581
+ const bundleInput = bundleWasSupplied ? String(bundleFlag) : DEFAULT_CROSS_MODEL_DEMO_BUNDLE;
582
+ const bundlePath = resolve(bundleInput);
583
+ const out = getFlag(flags, ['out']);
584
+ if (out === true || out === '') throw new Error('Missing required --out.');
585
+ const outPath = out === undefined ? undefined : resolve(String(out));
586
+ if (outPath !== undefined) ensureDistinctOutputPaths([bundlePath, outPath]);
587
+
588
+ const memoryFileWasSupplied = getFlag(flags, ['memory-file', 'memoryFile', 'text-file', 'textFile']) !== undefined;
589
+ let bundleCreated = false;
590
+ let vault;
591
+ let passport;
592
+ let demoMemoryAddr;
593
+
594
+ if (!bundleWasSupplied) {
595
+ vault = createVault({
596
+ subjectId: 'cross-model-demo-user',
597
+ displayName: 'Cross-model demo user',
598
+ passphrase: 'local-cross-model-demo-passphrase',
599
+ });
600
+ passport = createPassport({ vault, subjectId: vault.subject_id, displayName: 'Cross-model demo user' });
601
+ const remembered = remember({
602
+ vault,
603
+ passport,
604
+ text: await crossModelMemoryTextFromFlags(flags),
605
+ purpose: 'cross_model_demo_memory',
606
+ purpose_tags: ['cross-model-demo'],
607
+ metadata: { source: memoryFileWasSupplied ? 'local file supplied to demo' : 'generic cross-model demo memory' },
608
+ });
609
+ demoMemoryAddr = remembered.memory_addr;
610
+ bundleCreated = true;
611
+ } else {
612
+ const existed = await fileExists(bundlePath);
613
+ if (!existed) {
614
+ await ensureBundle(bundlePath, flags);
615
+ bundleCreated = true;
616
+ }
617
+ ({ vault, passport } = await loadState(bundlePath));
618
+ const remembered = remember({
619
+ vault,
620
+ passport,
621
+ text: await crossModelMemoryTextFromFlags(flags),
622
+ purpose: 'cross_model_demo_memory',
623
+ purpose_tags: ['cross-model-demo'],
624
+ metadata: { source: memoryFileWasSupplied ? 'local file supplied to demo' : 'generic cross-model demo memory' },
625
+ });
626
+ demoMemoryAddr = remembered.memory_addr;
627
+ }
628
+ const memorySource = memoryFileWasSupplied ? 'memory_file' : 'generic_demo';
629
+
630
+ const limit = integerFlag(flags, ['limit'], 'limit', 1);
631
+ if (limit < 1) throw new Error('--limit must be at least 1.');
632
+ const receiptCountBeforeProfiles = Array.isArray(vault.receipts) ? vault.receipts.length : 0;
633
+ const profiles = [];
634
+ for (const profile of CROSS_MODEL_PROFILES) {
635
+ const pack = compileContextPack({
636
+ vault,
637
+ passport,
638
+ provider: profile.provider,
639
+ model: profile.model,
640
+ query: 'memory follows me across models',
641
+ purpose: `cross_model_demo:${profile.id}`,
642
+ memory_addresses: [demoMemoryAddr],
643
+ limit,
644
+ });
645
+ const contextPack = publicContextPackSummary(pack);
646
+ profiles.push({
647
+ profile: profile.id,
648
+ label: profile.label,
649
+ provider: profile.provider,
650
+ model: profile.model,
651
+ context_pack_ref: contextPack.context_pack_ref,
652
+ context_pack_id: contextPack.context_pack_id,
653
+ context_pack_digest: contextPack.context_pack_digest,
654
+ context_pack: contextPack,
655
+ receipt_count: contextPack.receipt_count,
656
+ memory_count: contextPack.memory_count,
657
+ provider_native_memory_canonical: false,
658
+ receipts: contextPack.receipts,
659
+ claim_boundaries: { ...CROSS_MODEL_CLAIM_BOUNDARIES },
660
+ });
661
+ }
662
+
663
+ const bundle = await persistState(bundlePath, vault);
664
+ const report = {
665
+ ok: true,
666
+ schema: 'enigma.cross_model_demo.v1',
667
+ command: 'enigma demo cross-model',
668
+ story: 'One local Enigma memory is packaged as public-safe context pack references and receipts for ChatGPT, Claude, Kimi, Cursor, and a local LLM. No provider is called.',
669
+ bundle_ref: demoBundleRef(bundleWasSupplied),
670
+ bundle_supplied: bundleWasSupplied,
671
+ bundle_created: bundleCreated,
672
+ demo_only_vault: !bundleWasSupplied,
673
+ memory_source: memorySource,
674
+ demo_memory_addr: demoMemoryAddr,
675
+ profile_count: profiles.length,
676
+ profiles,
677
+ memory_count: activeMemoryCount(vault),
678
+ receipt_count: Array.isArray(bundle.receipts) ? bundle.receipts.length : 0,
679
+ generated_receipt_count: (Array.isArray(bundle.receipts) ? bundle.receipts.length : 0) - receiptCountBeforeProfiles,
680
+ provider_credentials_required: false,
681
+ provider_native_memory_canonical: false,
682
+ out_written: outPath !== undefined,
683
+ claim_boundaries: { ...CROSS_MODEL_CLAIM_BOUNDARIES },
684
+ };
685
+ if (outPath !== undefined) await writeJson(outPath, report);
686
+ print(report, io);
687
+ return 0;
688
+ }
689
+
364
690
  async function rememberCommand(flags, io) {
365
691
  const bundlePath = resolve(String(getFlag(flags, ['bundle', 'file'], DEFAULT_BUNDLE)));
366
692
  const { vault, passport } = await loadState(bundlePath);
@@ -946,6 +1272,8 @@ function usage() {
946
1272
  usage: 'enigma <command> [options]',
947
1273
  commands: [
948
1274
  'init',
1275
+ 'quickstart',
1276
+ 'demo cross-model',
949
1277
  'doctor',
950
1278
  'install',
951
1279
  'connect <client>',
@@ -990,6 +1318,21 @@ function usage() {
990
1318
  '--text <text>': 'Inline local memory text. Avoid for private content because argv can be logged by process tooling.',
991
1319
  '--text-file <path>': 'Read local memory text from a file so private smoke input is not exposed in shell argv. Aliases: --memory-file, --textFile, --memoryFile.',
992
1320
  },
1321
+ quickstart_options: {
1322
+ '--bundle <path>': 'Bundle JSON to create. Defaults to .enigma/bundle.json.',
1323
+ '--out-dir <path>': 'Directory for context-pack.json, export.json, and verify-report.json. Defaults to the bundle directory.',
1324
+ '--subject <id>': 'Local subject id. Defaults to local-user.',
1325
+ '--display-name <name>': 'Local display name. Defaults to Local user.',
1326
+ '--memory-file <path>': 'Read local memory text from a file. Alias: --text-file.',
1327
+ '--memory-text <text>': 'Inline demo memory text for non-private demos only.',
1328
+ '--overwrite': 'Replace existing quickstart output files.',
1329
+ },
1330
+ cross_model_demo_options: {
1331
+ '--bundle <path>': `Reuse a local Enigma bundle. If omitted, ${DEFAULT_CROSS_MODEL_DEMO_BUNDLE} is recreated as a demo-only local vault.`,
1332
+ '--memory-file <path>': 'Seed the demo from a local file without echoing plaintext. Alias: --text-file.',
1333
+ '--out <path>': 'Write the same public-safe JSON report to a local file.',
1334
+ '--limit <n>': 'Maximum active memories per generated profile context pack. Defaults to 1 for the same-memory demo story.',
1335
+ },
993
1336
  native_host: {
994
1337
  bin: 'enigma-native-host',
995
1338
  host_name: 'com.enigma.native_host',
@@ -1055,15 +1398,17 @@ export async function main(argv = process.argv.slice(2), io = { stdout: process.
1055
1398
  print(usage(), io);
1056
1399
  return 0;
1057
1400
  }
1058
- const twoPartCommands = ['boundary', 'mcp', 'mesh', 'enterprise', 'capsule', 'relay', 'gateway', 'connect', 'disconnect', 'import', 'native-host', 'meter', 'settlement'];
1401
+ const twoPartCommands = ['boundary', 'mcp', 'mesh', 'enterprise', 'capsule', 'relay', 'gateway', 'connect', 'disconnect', 'import', 'native-host', 'meter', 'settlement', 'demo'];
1059
1402
  const flags = parseArgs(twoPartCommands.includes(command) ? argv.slice(2) : argv.slice(1));
1060
1403
  const positionalFile = optionalPositional(argv[2]);
1061
- if ((flags.has('help') || argv.includes('-h')) && (((command === 'relay' || command === 'gateway') && (subcommand === 'serve' || subcommand === 'demo')) || (command === 'native-host' && (subcommand === 'manifest' || subcommand === 'install-plan')))) {
1404
+ if ((flags.has('help') || argv.includes('-h')) && (((command === 'relay' || command === 'gateway') && (subcommand === 'serve' || subcommand === 'demo')) || (command === 'native-host' && (subcommand === 'manifest' || subcommand === 'install-plan')) || (command === 'demo' && subcommand === 'cross-model'))) {
1062
1405
  print(usage(), io);
1063
1406
  return 0;
1064
1407
  }
1065
1408
  try {
1066
1409
  if (command === 'init') return await initCommand(flags, io);
1410
+ if (command === 'quickstart') return await quickstartCommand(flags, io);
1411
+ if (command === 'demo' && subcommand === 'cross-model') return await crossModelDemoCommand(flags, io);
1067
1412
  if (command === 'doctor') return await doctorCommand(flags, io);
1068
1413
  if (command === 'install') return await installCommand(flags, io);
1069
1414
  if (command === 'connect') return await connectCommand(subcommand, flags, io);