@anuma/sdk 2.1.0 → 2.2.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.
@@ -53,6 +53,7 @@ import {
53
53
  getConversationsLazyOp,
54
54
  getConversationsOp,
55
55
  getEncryptionKey,
56
+ getMemoriesNeedingTopicExtractionOp,
56
57
  getMessagesOp,
57
58
  getUnfiledVaultMemoriesOp,
58
59
  getVaultMemoryOp,
@@ -66,6 +67,7 @@ import {
66
67
  onKeyAvailable,
67
68
  preEmbedVaultMemories,
68
69
  recall,
70
+ replaceMemoryEntitiesGuardedOp,
69
71
  requestEncryptionKey,
70
72
  requestKeyPair,
71
73
  require_QueryDescription,
@@ -85,6 +87,7 @@ import {
85
87
  searchMessagesOp,
86
88
  searchVaultMemories,
87
89
  shouldChunkMessage,
90
+ stampTopicsExtractedAtOp,
88
91
  stripToolCalls,
89
92
  updateConversationProjectOp,
90
93
  updateConversationTitleOp,
@@ -94,8 +97,9 @@ import {
94
97
  updateMessageFeedbackOp,
95
98
  updateVaultMemoryEmbeddingOp,
96
99
  updateVaultMemoryOp,
97
- upsertMessageOp
98
- } from "./chunk-45GOZ3AB.mjs";
100
+ upsertMessageOp,
101
+ vaultMemoryToStored
102
+ } from "./chunk-3AWYZRTS.mjs";
99
103
  import {
100
104
  createStreamingDeAnonymizer,
101
105
  resolvePiiRedactor
@@ -1086,6 +1090,9 @@ __decorateClass([
1086
1090
  __decorateClass([
1087
1091
  (0, import_decorators6.field)("topics_user_managed")
1088
1092
  ], VaultMemory.prototype, "topicsUserManaged", 2);
1093
+ __decorateClass([
1094
+ (0, import_decorators6.field)("topics_extracted_at")
1095
+ ], VaultMemory.prototype, "topicsExtractedAt", 2);
1089
1096
  __decorateClass([
1090
1097
  import_decorators6.readonly,
1091
1098
  (0, import_decorators6.date)("created_at")
@@ -1233,7 +1240,7 @@ __decorateClass([
1233
1240
  ], VaultFolder.prototype, "context", 2);
1234
1241
 
1235
1242
  // src/lib/db/schema.ts
1236
- var SDK_SCHEMA_VERSION = 35;
1243
+ var SDK_SCHEMA_VERSION = 36;
1237
1244
  var sdkSchema = (0, import_watermelondb12.appSchema)({
1238
1245
  version: SDK_SCHEMA_VERSION,
1239
1246
  tables: [
@@ -1354,7 +1361,11 @@ var sdkSchema = (0, import_watermelondb12.appSchema)({
1354
1361
  // When true, the user has manually set this memory's topics (entity
1355
1362
  // links). Auto-extraction then leaves its links alone — the user owns
1356
1363
  // them. Null/false = topics are auto-derived (default).
1357
- { name: "topics_user_managed", type: "boolean", isOptional: true }
1364
+ { name: "topics_user_managed", type: "boolean", isOptional: true },
1365
+ // Unix ms of the last LLM topic-extraction pass over this memory's
1366
+ // content. Null = never extracted standalone (legacy rows with entity
1367
+ // links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
1368
+ { name: "topics_extracted_at", type: "number", isOptional: true }
1358
1369
  ]
1359
1370
  }),
1360
1371
  // Entity table — canonical names extracted from auto-extraction (W5).
@@ -1952,6 +1963,19 @@ var sdkMigrations = (0, import_migrations.schemaMigrations)({
1952
1963
  ]
1953
1964
  })
1954
1965
  ]
1966
+ },
1967
+ // v35 -> v36: topics_extracted_at watermark on memory_vault. Existing rows
1968
+ // keep it NULL — rows that already have entity links are grandfathered as
1969
+ // extracted (no mass re-extraction on upgrade), rows without links are the
1970
+ // backfill target. See getMemoriesNeedingTopicExtractionOp.
1971
+ {
1972
+ toVersion: 36,
1973
+ steps: [
1974
+ (0, import_migrations.addColumns)({
1975
+ table: "memory_vault",
1976
+ columns: [{ name: "topics_extracted_at", type: "number", isOptional: true }]
1977
+ })
1978
+ ]
1955
1979
  }
1956
1980
  ]
1957
1981
  });
@@ -4839,7 +4863,7 @@ async function isMemoryTopicsUserManaged(ctx, memoryId) {
4839
4863
  return true;
4840
4864
  }
4841
4865
  }
4842
- var DEFAULT_MODEL = "gpt-oss/gpt-oss-120b";
4866
+ var DEFAULT_EXTRACTION_MODEL = "gpt-oss/gpt-oss-120b";
4843
4867
  var DEFAULT_MIN_CONFIDENCE = 0.7;
4844
4868
  var MAX_CONTENT_LENGTH = 200;
4845
4869
  var MIN_CONTENT_LENGTH = 3;
@@ -4852,6 +4876,16 @@ var FACT_TYPES = [
4852
4876
  "constraint",
4853
4877
  "other"
4854
4878
  ];
4879
+ var ENTITY_KIND_GUIDELINES = `Choose the MOST SPECIFIC kind:
4880
+ - person: a named individual ("Sara", "Dr. Lee")
4881
+ - organization: a company, team, school, or group ("Google", "the Warriors", "Stanford")
4882
+ - place: a geographic location or venue ("San Francisco", "Blue Bottle on Valencia")
4883
+ - event: a dated or nameable occurrence ("Chicago Marathon", "the Japan trip", "their wedding")
4884
+ - product: a named app, tool, brand, device, book, film, or other media title ("Linear", "iPhone", "Dune", "Ableton")
4885
+ - thing: a concrete physical object that isn't a branded product ("road bike", "sourdough starter")
4886
+ - concept: a topic, skill, field, language, or idea ("machine learning", "Spanish", "async communication")
4887
+ - other: a named entity that genuinely fits none of the above \u2014 use sparingly
4888
+ Prefer organization / product / event / place over the generic "thing"; fall back to "thing" or "other" only when no specific kind fits.`;
4855
4889
  var SYSTEM_PROMPT = `You extract durable user facts from conversations for a personal memory system.
4856
4890
 
4857
4891
  A "durable fact" is something the user shared about themselves that should still be true in future conversations. Examples that ARE durable:
@@ -4880,16 +4914,7 @@ For each durable fact, output:
4880
4914
  - type: one of identity | preference | relationship | plan | ongoing_context | constraint | other
4881
4915
  - confidence: 0.0-1.0; how sure you are this is durable AND true. Only include facts >= 0.7.
4882
4916
  - sourceMessageIds: which message IDs from the conversation contained the evidence
4883
- - entities: named entities mentioned, each as an object { "name": string, "kind": one of "person" | "organization" | "place" | "event" | "product" | "thing" | "concept" | "other" }. Optional \u2014 include only NAMED entities, skip generic/common nouns. Choose the MOST SPECIFIC kind:
4884
- - person: a named individual ("Sara", "Dr. Lee")
4885
- - organization: a company, team, school, or group ("Google", "the Warriors", "Stanford")
4886
- - place: a geographic location or venue ("San Francisco", "Blue Bottle on Valencia")
4887
- - event: a dated or nameable occurrence ("Chicago Marathon", "the Japan trip", "their wedding")
4888
- - product: a named app, tool, brand, device, book, film, or other media title ("Linear", "iPhone", "Dune", "Ableton")
4889
- - thing: a concrete physical object that isn't a branded product ("road bike", "sourdough starter")
4890
- - concept: a topic, skill, field, language, or idea ("machine learning", "Spanish", "async communication")
4891
- - other: a named entity that genuinely fits none of the above \u2014 use sparingly
4892
- Prefer organization / product / event / place over the generic "thing"; fall back to "thing" or "other" only when no specific kind fits. Examples:
4917
+ - entities: named entities mentioned, each as an object { "name": string, "kind": one of "person" | "organization" | "place" | "event" | "product" | "thing" | "concept" | "other" }. Optional \u2014 include only NAMED entities, skip generic/common nouns. ${ENTITY_KIND_GUIDELINES} Examples:
4893
4918
  - "started a new job at Hollowpoint Labs and I'm learning Haskell for it" \u2192 entities: [{ "name": "Hollowpoint Labs", "kind": "organization" }, { "name": "Haskell", "kind": "concept" }]
4894
4919
  - "running the Berlin Half in May and I track workouts in Whoop" \u2192 entities: [{ "name": "Berlin Half", "kind": "event" }, { "name": "Whoop", "kind": "product" }]
4895
4920
  - eventTime: when the event in this fact occurs/occurred. Resolve relative phrases ("yesterday", "next week") against the current date given in the user message. Output one of:
@@ -4915,7 +4940,7 @@ async function extractFacts(messages, options) {
4915
4940
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
4916
4941
  ...options.getToken !== void 0 && { getToken: options.getToken },
4917
4942
  ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
4918
- model: options.model ?? DEFAULT_MODEL,
4943
+ model: options.model ?? DEFAULT_EXTRACTION_MODEL,
4919
4944
  systemPrompt: SYSTEM_PROMPT,
4920
4945
  userMessage: `Today's date is ${today}. Resolve any relative dates ("yesterday", "next week") against it.
4921
4946
 
@@ -5009,7 +5034,9 @@ async function extractAndRetain(messages, retainCtx, options) {
5009
5034
  if (options.entityCtx && candidate.entities.length > 0) {
5010
5035
  try {
5011
5036
  if (!await isMemoryTopicsUserManaged(options.entityCtx, result.memoryId)) {
5012
- await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities);
5037
+ await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities, {
5038
+ unlessTopicsUserManaged: true
5039
+ });
5013
5040
  }
5014
5041
  } catch (err) {
5015
5042
  log.warn("[memory/extract] linkMemoryEntitiesOp failed", err);
@@ -5404,7 +5431,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
5404
5431
 
5405
5432
  // src/lib/memory/reflect.ts
5406
5433
  var DEFAULT_BASE_URL = BASE_URL;
5407
- var DEFAULT_MODEL2 = "anthropic/claude-sonnet-4-6";
5434
+ var DEFAULT_MODEL = "anthropic/claude-sonnet-4-6";
5408
5435
  var DEFAULT_MAX_TOKENS = 4096;
5409
5436
  var REQUEST_TIMEOUT_MS = 6e4;
5410
5437
  var DEFAULT_SYSTEM_PROMPT = `You are a personal assistant with access to the user's memory. Answer the user's question using the supplied memories as evidence.
@@ -5436,7 +5463,7 @@ async function reflect(query, ctx, options) {
5436
5463
  const evidence = recalled.memories.map((m, i) => `[${i + 1}] (id: ${m.id}, kind: ${m.kind})
5437
5464
  ${m.content}`).join("\n\n");
5438
5465
  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
5439
- const model = options.llmModel ?? DEFAULT_MODEL2;
5466
+ const model = options.llmModel ?? DEFAULT_MODEL;
5440
5467
  const wantsStructured = !!options.responseSchema;
5441
5468
  const sendResponseFormat = wantsStructured && supportsResponseFormat(model, "json_schema");
5442
5469
  const basePrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
@@ -5546,6 +5573,163 @@ function parseAnswer(body, base, parseSchema) {
5546
5573
  };
5547
5574
  }
5548
5575
 
5576
+ // src/lib/memory/topicExtract.ts
5577
+ var TOPIC_EXTRACTION_BATCH_SIZE = 10;
5578
+ var MAX_CHARS_PER_MEMORY = 300;
5579
+ var MAX_VOCABULARY_NAMES = 100;
5580
+ var SYSTEM_PROMPT2 = `You assign topics to saved memories for a personal memory system.
5581
+
5582
+ Each memory is a short statement about the user. For each memory, list the NAMED entities it mentions \u2014 these become the memory's topics, used to connect related memories in a knowledge graph.
5583
+
5584
+ Include only NAMED entities, skip generic/common nouns. ${ENTITY_KIND_GUIDELINES}
5585
+
5586
+ Output strict JSON: {"memories": [{"id": string, "entities": [{"name": string, "kind": string}]}]}
5587
+ - exactly one element per input memory, echoing its id verbatim
5588
+ - "entities" may be empty when a memory mentions no named entities \u2014 most short memories have 0-3
5589
+ No prose.`;
5590
+ async function extractEntitiesForMemories(memories, options) {
5591
+ const out = /* @__PURE__ */ new Map();
5592
+ if (memories.length === 0) return out;
5593
+ const redactor = resolvePiiRedactor(options.piiRedaction);
5594
+ const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
5595
+ const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
5596
+ When a memory refers to one of these, reuse the EXACT existing name (same spelling and casing) instead of a variant.
5597
+
5598
+ ` : "";
5599
+ for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
5600
+ const batch = memories.slice(i, i + TOPIC_EXTRACTION_BATCH_SIZE);
5601
+ const listing = batch.map((m) => {
5602
+ const content = m.content.slice(0, MAX_CHARS_PER_MEMORY);
5603
+ return `[${m.id}] ${redactor ? redactor.redactText(content).text : content}`;
5604
+ }).join("\n");
5605
+ const parsed = await callPortalJsonCompletion({
5606
+ ...options.apiKey !== void 0 && { apiKey: options.apiKey },
5607
+ ...options.getToken !== void 0 && { getToken: options.getToken },
5608
+ ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
5609
+ model: options.model ?? DEFAULT_EXTRACTION_MODEL,
5610
+ systemPrompt: SYSTEM_PROMPT2,
5611
+ userMessage: `${vocabularyNote}Memories:
5612
+ ${listing}
5613
+
5614
+ List each memory's named entities.`,
5615
+ tag: "memory/topics",
5616
+ ...options.fetchFn && { fetchFn: options.fetchFn },
5617
+ ...options.maxAttempts !== void 0 && { maxAttempts: options.maxAttempts },
5618
+ ...options.timeoutMs !== void 0 && { timeoutMs: options.timeoutMs },
5619
+ ...options.totalTimeoutMs !== void 0 && { totalTimeoutMs: options.totalTimeoutMs },
5620
+ ...options.backoffMs && { backoffMs: options.backoffMs }
5621
+ });
5622
+ if (parsed === null) {
5623
+ getLogger().warn(
5624
+ `[memory/topics] batch of ${batch.length} failed after retries \u2014 will retry next sweep`
5625
+ );
5626
+ continue;
5627
+ }
5628
+ const validIds = new Set(batch.map((m) => m.id));
5629
+ const byId = parseTopicResponse(parsed, validIds);
5630
+ if (byId === null) {
5631
+ getLogger().warn(
5632
+ `[memory/topics] batch of ${batch.length} returned an unrecognized shape \u2014 will retry next sweep`
5633
+ );
5634
+ continue;
5635
+ }
5636
+ for (const m of batch) {
5637
+ const entities = byId.get(m.id);
5638
+ if (entities === void 0) {
5639
+ continue;
5640
+ }
5641
+ out.set(m.id, redactor ? restoreEntities(entities, redactor) : entities);
5642
+ }
5643
+ }
5644
+ return out;
5645
+ }
5646
+ function restoreEntities(entities, redactor) {
5647
+ return entities.map((e) => ({ kind: e.kind, restored: redactor.restoreForStorage(e.name) })).filter((e) => !e.restored.unresolved).map(
5648
+ (e) => e.kind !== void 0 ? { name: e.restored.text, kind: e.kind } : { name: e.restored.text }
5649
+ );
5650
+ }
5651
+ function parseTopicResponse(parsed, validIds) {
5652
+ if (typeof parsed !== "object" || parsed === null) return null;
5653
+ const list = parsed.memories;
5654
+ if (!Array.isArray(list)) return null;
5655
+ const out = /* @__PURE__ */ new Map();
5656
+ for (const raw of list) {
5657
+ if (typeof raw !== "object" || raw === null) continue;
5658
+ const obj = raw;
5659
+ if (typeof obj.id !== "string" || !validIds.has(obj.id) || out.has(obj.id)) continue;
5660
+ const entities = Array.isArray(obj.entities) ? parseEntities(obj.entities) : [];
5661
+ out.set(obj.id, entities);
5662
+ }
5663
+ return out;
5664
+ }
5665
+ async function extractAndLinkEntitiesForMemoriesOp(ctx, memoryIds, options) {
5666
+ const entityCtx = ctx.entityCtx;
5667
+ if (!entityCtx) {
5668
+ throw new Error("extractAndLinkEntitiesForMemoriesOp requires ctx.entityCtx");
5669
+ }
5670
+ const log = getLogger();
5671
+ const extractedAt = options.now ?? Date.now();
5672
+ const skippedIds = [];
5673
+ const inputs = [];
5674
+ for (const id of memoryIds) {
5675
+ let record;
5676
+ try {
5677
+ record = await ctx.vaultMemoryCollection.find(id);
5678
+ } catch {
5679
+ skippedIds.push(id);
5680
+ continue;
5681
+ }
5682
+ if (record.isDeleted || ctx.userId !== void 0 && record.userId !== ctx.userId || record.topicsUserManaged) {
5683
+ skippedIds.push(id);
5684
+ continue;
5685
+ }
5686
+ try {
5687
+ const stored = await vaultMemoryToStored(
5688
+ record,
5689
+ ctx.walletAddress,
5690
+ ctx.signMessage,
5691
+ ctx.embeddedWalletSigner
5692
+ );
5693
+ inputs.push({ id, content: stored.content });
5694
+ } catch (err) {
5695
+ log.warn("[memory/topics] failed to load memory for extraction", err);
5696
+ skippedIds.push(id);
5697
+ }
5698
+ }
5699
+ const entitiesByMemory = await extractEntitiesForMemories(inputs, options);
5700
+ const toStamp = [];
5701
+ for (const input of inputs) {
5702
+ const entities = entitiesByMemory.get(input.id);
5703
+ if (entities === void 0) {
5704
+ skippedIds.push(input.id);
5705
+ continue;
5706
+ }
5707
+ try {
5708
+ const linked = await replaceMemoryEntitiesGuardedOp(entityCtx, input.id, entities);
5709
+ if (linked === null) {
5710
+ skippedIds.push(input.id);
5711
+ entitiesByMemory.delete(input.id);
5712
+ continue;
5713
+ }
5714
+ } catch (err) {
5715
+ log.warn("[memory/topics] replaceMemoryEntitiesGuardedOp failed", err);
5716
+ skippedIds.push(input.id);
5717
+ entitiesByMemory.delete(input.id);
5718
+ continue;
5719
+ }
5720
+ toStamp.push(input.id);
5721
+ }
5722
+ const stampedIds = await stampTopicsExtractedAtOp(ctx, toStamp, extractedAt);
5723
+ const stampedSet = new Set(stampedIds);
5724
+ for (const id of toStamp) {
5725
+ if (!stampedSet.has(id)) {
5726
+ skippedIds.push(id);
5727
+ entitiesByMemory.delete(id);
5728
+ }
5729
+ }
5730
+ return { entitiesByMemory, stampedIds, skippedIds };
5731
+ }
5732
+
5549
5733
  // src/tools/appBuilderPrompt.ts
5550
5734
  var APP_BUILDER_PROMPT = `App Builder tools (create_file, patch_file, and the rest described below) are available this turn, but they are OPTIONAL \u2014 having them does not mean you should build anything.
5551
5735
 
@@ -11087,6 +11271,7 @@ export {
11087
11271
  STREAM_RESUMABLE_HEADER,
11088
11272
  StreamExpiredError,
11089
11273
  TOOL_CATALOG,
11274
+ TOPIC_EXTRACTION_BATCH_SIZE,
11090
11275
  TextProcessor,
11091
11276
  UserPreference,
11092
11277
  VaultFolder,
@@ -11167,7 +11352,9 @@ export {
11167
11352
  estimateMessagesTokens,
11168
11353
  estimateTokens,
11169
11354
  exportPublicKey,
11355
+ extractAndLinkEntitiesForMemoriesOp,
11170
11356
  extractAndRetain,
11357
+ extractEntitiesForMemories,
11171
11358
  extractFacts,
11172
11359
  findMatchingTools,
11173
11360
  generateConversationId,
@@ -11203,6 +11390,7 @@ export {
11203
11390
  getMediaCountsByTypeOp,
11204
11391
  getMediaOp,
11205
11392
  getMediaTypeFromMime,
11393
+ getMemoriesNeedingTopicExtractionOp,
11206
11394
  getMessagesOp,
11207
11395
  getModelPreferenceOp,
11208
11396
  getProjectConversationCountOp,
@@ -11262,6 +11450,7 @@ export {
11262
11450
  shouldChunkMessage,
11263
11451
  shouldRefreshTools,
11264
11452
  splitMessagesAtThreshold,
11453
+ stampTopicsExtractedAtOp,
11265
11454
  streamCancelPath,
11266
11455
  streamReplayPath,
11267
11456
  summaryToSystemMessage,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-45GOZ3AB.mjs";
3
+ } from "./chunk-3AWYZRTS.mjs";
4
4
  import "./chunk-AHJT66EO.mjs";
5
5
  import "./chunk-3RPGXXPZ.mjs";
6
6
  export {
@@ -1516,6 +1516,11 @@ declare function exportAppToHtml(options: ExportAppOptions): string;
1516
1516
  * The workflow this prompt mandates (create → critique → patch → audit →
1517
1517
  * patch → verify) needs 7–8 tool rounds on a substantial turn. Hosts capping
1518
1518
  * `maxToolRounds` below 8 starve the trailing `verify_app` step.
1519
+ *
1520
+ * ⚠ The substring "App Builder tools (create_file, patch_file," is relied on by
1521
+ * backend infrastructure — matched anywhere in the text (substring), not as a
1522
+ * prefix/head — so keep it byte-identical; see internal docs.
1523
+ * Guarded by the backend-sync test in appBuilderPrompt.test.ts.
1519
1524
  */
1520
1525
  declare const APP_BUILDER_PROMPT = "App Builder tools (create_file, patch_file, and the rest described below) are available this turn, but they are OPTIONAL \u2014 having them does not mean you should build anything.\n\nBuild ONLY when the user gives an explicit instruction to build, create, generate, or change an app, tool, or UI \u2014 e.g. \"build a todo app\", \"make a snake game\", \"add a footer to the app\", \"change the background to blue\". A bare app name or feature reference is NOT a build instruction: if the user just says \"todo app\", \"a snake game\", or \"qr code\", or asks a question about an app, do NOT generate anything \u2014 reply normally, or ask what they'd like you to build. The same holds for greetings, questions, stories, explanations, advice, or code that isn't a runnable app: respond normally and do not create files. When it's unclear whether the user actually wants an app built, ask instead of building.\n\nOnce the user HAS given an explicit build instruction, you are in App Builder mode: you produce polished, production-quality React apps that feel designed \u2014 not generic.\n\nDESIGN: on the initial build, before writing code, briefly state (2-3 sentences) what you want this app to feel like \u2014 name the specific fonts, the palette in hex, and one signature detail. Be concrete: \"Fraunces display + Inter body + JetBrains Mono metadata, terracotta #b75432 accent on bone #f3efe6, paper-grain texture via inline SVG noise\" is the kind of specificity that anchors the build. Default to nothing: don't pick \"safe modern productivity\" unless the brief literally asks for utilitarian. On later turns, state a design brief only when the change is visual (a restyle, new theme, or new UI); for a logic, data, or behavior-only change, skip the brief and just make the change.\n\nWORKFLOW:\n1. NEVER output code as text or markdown. ALWAYS use tools.\n2. Initial build: state your design brief, then create_file with a \"files\" array writing ALL files in one call. The preview renders automatically.\n3. patch_file requires read_file for that path earlier in the conversation (or create_file in the same conversation, which counts). read_file output is numbered \"42: <text>\" \u2014 the prefix is display-only; never copy it into \"find\". Each \"find\" must match exactly one location \u2014 include 2-3 lines of surrounding context. Pass multiple patches per call to change distinct locations.\n4. Prefer patch_file for changes to existing files \u2014 surgical, reviewable, preserves surrounding code. Reserve create_file overwrites for substantial restructuring. Inserts: find the line before, replace with that line + the new line. Deletes: empty replace.\n5. After substantial changes, call critique_design first (taste, hierarchy, distinctiveness, weakest decision) and then audit_design (mechanical: tokens, focus states, aria-labels, alt, heading order). Patch what each surfaces. critique catches taste issues no checklist would; audit catches mechanics you'd miss. Run both; act on both.\n6. Before declaring done, call verify_app. The host's preview runtime reports whether your app actually mounted and surfaces any errors it captured \u2014 hallucinated imports, syntax the bundler rejected, undefined components, runtime crashes. If `rendered` is false or `errors` is non-empty, fix every error before the next message. If the response includes a `note` that verify_app isn't wired in this host, skip \u2014 audit + critique are your feedback in that case.\n7. Keep responses to a sentence or two. Exception: when responding to critique_design, write a substantive paragraph per rubric question.\n\nSTRUCTURE:\n- App.js: default-export React component. For non-trivial apps, define small co-located helpers (Header, CardList, Card, EditDialog, \u2026) above the default export rather than one giant App.\n- App.css: all styles, imported in App.js. No inline style objects.\n- package.json: list ALL imported packages including react. No CDN script tags. Versions are auto-pinned.\n- Do NOT create index.js or index.html (auto-generated).\n\nSTATE & PERSISTENCE:\n- The code runs in the user's browser \u2014 standard browser APIs available (localStorage, URL hash, Date, fetch).\n- Persist user-created data (todos, cards, notes, settings) with localStorage so it survives refresh. Use a versioned key (\"kanban.v1\") and JSON.parse inside try/catch on read.\n- Treat persisted data as untrusted and possibly stale: a saved record may predate a field you just added (a timestamp, id, or setting), so default-fill or skip missing fields on read \u2014 never assume a loaded record has every field the current code expects. Bump the key version when the shape changes.\n- Lift state to App when multiple children need it; keep it local otherwise. Extract non-trivial logic into custom hooks (useTodos, useTimer).\n- useReducer for state shapes with several interrelated fields (cards + columns + filters + drag state).\n\nFILE & IMAGE UPLOAD:\n- Picker: `<input type=\"file\" accept=\"image/*\" multiple>` (vary accept by use case: `.csv,text/csv`, `.txt,.md,text/plain`, etc.).\n- Drag-and-drop: call `e.preventDefault()` in onDragOver (otherwise drop never fires); read files from `event.dataTransfer.files`. Drop and picker should coexist.\n- Previews: `<img src={URL.createObjectURL(file)} />`. Call `URL.revokeObjectURL` on unmount or before replacing \u2014 otherwise the browser leaks memory.\n- localStorage cannot hold large binary. Persist only metadata for big files; re-upload on next visit (or use IndexedDB if you really need durable binary storage).\n- Validate size and type before processing; reject files larger than the app can hold in memory.\n\nAI CAPABILITIES \u2014 `window.app.complete(prompt: string): Promise<string>` is available in this environment. Build for the real call.\n- When the app's core purpose is conversation, generation, grading, recommendation, or any natural-language task, this IS the implementation \u2014 call it. NEVER fake it: no hardcoded or canned replies, no random or `setTimeout` \"thinking\", no demo-mode stub. A chat app, tutor, or assistant that doesn't call `window.app.complete` is wrong, not a placeholder.\n- Don't force it where it isn't needed \u2014 a calculator, todo list, or timer has no reason to call it. Match the implementation to what the app actually does.\n- Async: wrap in try/catch, show a loading state, and disable dependent inputs while waiting. Surface failures gracefully (\"Couldn't reach the AI \u2014 try again\").\n- Structure prompts with clear sections (Role / Context / Task / Format) and pass the relevant state (e.g. the running conversation) as context. Render the response as untrusted text: `white-space: pre-wrap`, never dangerouslySetInnerHTML.\n- Skip the call only when the answer is genuinely static \u2014 no language or reasoning needed. Cache repeated prompts in component state when sensible.\n\nCODE QUALITY \u2014 write as a senior engineer would:\n- Handle edge cases (empty inputs, division by zero, invalid data). Show helpful error states, not crashes.\n- Never let formatting throw during render: guard `new Date(x)` / `Intl` / `toLocaleString` / `.toFixed` against missing or invalid values (e.g. `Number.isNaN(d.getTime())`) and return a fallback \u2014 one bad value from storage or state must not blank the whole app.\n- Semantic HTML: real headings, labels, buttons (not divs with onClick), form elements.\n- Accessible: aria-labels on icon buttons, focus-visible styles, keyboard navigable.\n- Extract into custom hooks or helpers when a component grows past ~80 lines.\n\nSTYLING \u2014 Tailwind is the CHASSIS, custom CSS carries the IDENTITY:\n- Tailwind (via Play CDN) for layout, spacing, state (hover/focus), responsive prefixes \u2014 uniform structure.\n- App.css carries the identity: @import the Google Fonts, declare CSS custom properties (--bg, --panel, --card, --ink, --muted, --accent, --hairline, --radius, --radius-sm, --shadow-card, --shadow-card-hi) in :root, gradients/textures/signature animations Tailwind can't express. Reference tokens from Tailwind via arbitrary values (`className=\"bg-[var(--bg)]\"`) when needed. A well-designed app has 60-200 lines of App.css \u2014 not zero.\n- Modern CSS is how apps stop reading as generic-AI-output: `color-mix(in oklab, var(--accent) 14%, transparent)` for tinting, `oklch()` / `oklab()` for perceptually-uniform color, `:has()`, `conic-gradient` / `radial-gradient`, `backdrop-filter`, `@container` queries, inline SVG `feTurbulence` for texture.\n- Don't pull additional CSS frameworks. Tailwind + custom CSS is enough.\n\nVISUAL DESIGN \u2014 every app should look like a real product, not a wireframe:\n- Let the chosen aesthetic drive every decision. \"Editorial\" wants italic display type and restrained color; \"retro arcade\" wants chunky shadows and saturated gradients. No safe-modern defaults.\n- Atmospheric depth: subtle radial wash, paper grain (inline SVG noise), warm/cool tint \u2014 every canvas should feel like a designed space, not a #fff rectangle.\n- Three shadow tiers (rest / hover / active). Pair drop shadows with inset highlights (`box-shadow: 0 1px 0 rgba(255,255,255,.9) inset, 0 6px 14px -10px ...`) for tactile feel.\n- Typography hierarchy: display font for headlines (italic accents on key words via `<em>`), body font for content, mono for metadata/labels \u2014 different families telegraph different information types.\n- Interactive polish: hover/active/focus on every clickable element, 200-300ms cubic-bezier transitions, keyboard shortcuts where natural (Esc to cancel, Cmd/Ctrl+Enter to commit), focus-visible rings keyed to the accent.\n- Light/dark mode if reasonable: a single CSS variable inversion under `[data-theme=\"dark\"]`.\n- Customization affordance for multi-session apps: small \"Tweaks\" panel with theme / density / accent. Skip for throwaway demos.\n- Responsive: Tailwind responsive prefixes (`sm:`, `md:`, `lg:`) and `@media` in App.css. Mobile-first.";
1521
1526
 
@@ -1516,6 +1516,11 @@ declare function exportAppToHtml(options: ExportAppOptions): string;
1516
1516
  * The workflow this prompt mandates (create → critique → patch → audit →
1517
1517
  * patch → verify) needs 7–8 tool rounds on a substantial turn. Hosts capping
1518
1518
  * `maxToolRounds` below 8 starve the trailing `verify_app` step.
1519
+ *
1520
+ * ⚠ The substring "App Builder tools (create_file, patch_file," is relied on by
1521
+ * backend infrastructure — matched anywhere in the text (substring), not as a
1522
+ * prefix/head — so keep it byte-identical; see internal docs.
1523
+ * Guarded by the backend-sync test in appBuilderPrompt.test.ts.
1519
1524
  */
1520
1525
  declare const APP_BUILDER_PROMPT = "App Builder tools (create_file, patch_file, and the rest described below) are available this turn, but they are OPTIONAL \u2014 having them does not mean you should build anything.\n\nBuild ONLY when the user gives an explicit instruction to build, create, generate, or change an app, tool, or UI \u2014 e.g. \"build a todo app\", \"make a snake game\", \"add a footer to the app\", \"change the background to blue\". A bare app name or feature reference is NOT a build instruction: if the user just says \"todo app\", \"a snake game\", or \"qr code\", or asks a question about an app, do NOT generate anything \u2014 reply normally, or ask what they'd like you to build. The same holds for greetings, questions, stories, explanations, advice, or code that isn't a runnable app: respond normally and do not create files. When it's unclear whether the user actually wants an app built, ask instead of building.\n\nOnce the user HAS given an explicit build instruction, you are in App Builder mode: you produce polished, production-quality React apps that feel designed \u2014 not generic.\n\nDESIGN: on the initial build, before writing code, briefly state (2-3 sentences) what you want this app to feel like \u2014 name the specific fonts, the palette in hex, and one signature detail. Be concrete: \"Fraunces display + Inter body + JetBrains Mono metadata, terracotta #b75432 accent on bone #f3efe6, paper-grain texture via inline SVG noise\" is the kind of specificity that anchors the build. Default to nothing: don't pick \"safe modern productivity\" unless the brief literally asks for utilitarian. On later turns, state a design brief only when the change is visual (a restyle, new theme, or new UI); for a logic, data, or behavior-only change, skip the brief and just make the change.\n\nWORKFLOW:\n1. NEVER output code as text or markdown. ALWAYS use tools.\n2. Initial build: state your design brief, then create_file with a \"files\" array writing ALL files in one call. The preview renders automatically.\n3. patch_file requires read_file for that path earlier in the conversation (or create_file in the same conversation, which counts). read_file output is numbered \"42: <text>\" \u2014 the prefix is display-only; never copy it into \"find\". Each \"find\" must match exactly one location \u2014 include 2-3 lines of surrounding context. Pass multiple patches per call to change distinct locations.\n4. Prefer patch_file for changes to existing files \u2014 surgical, reviewable, preserves surrounding code. Reserve create_file overwrites for substantial restructuring. Inserts: find the line before, replace with that line + the new line. Deletes: empty replace.\n5. After substantial changes, call critique_design first (taste, hierarchy, distinctiveness, weakest decision) and then audit_design (mechanical: tokens, focus states, aria-labels, alt, heading order). Patch what each surfaces. critique catches taste issues no checklist would; audit catches mechanics you'd miss. Run both; act on both.\n6. Before declaring done, call verify_app. The host's preview runtime reports whether your app actually mounted and surfaces any errors it captured \u2014 hallucinated imports, syntax the bundler rejected, undefined components, runtime crashes. If `rendered` is false or `errors` is non-empty, fix every error before the next message. If the response includes a `note` that verify_app isn't wired in this host, skip \u2014 audit + critique are your feedback in that case.\n7. Keep responses to a sentence or two. Exception: when responding to critique_design, write a substantive paragraph per rubric question.\n\nSTRUCTURE:\n- App.js: default-export React component. For non-trivial apps, define small co-located helpers (Header, CardList, Card, EditDialog, \u2026) above the default export rather than one giant App.\n- App.css: all styles, imported in App.js. No inline style objects.\n- package.json: list ALL imported packages including react. No CDN script tags. Versions are auto-pinned.\n- Do NOT create index.js or index.html (auto-generated).\n\nSTATE & PERSISTENCE:\n- The code runs in the user's browser \u2014 standard browser APIs available (localStorage, URL hash, Date, fetch).\n- Persist user-created data (todos, cards, notes, settings) with localStorage so it survives refresh. Use a versioned key (\"kanban.v1\") and JSON.parse inside try/catch on read.\n- Treat persisted data as untrusted and possibly stale: a saved record may predate a field you just added (a timestamp, id, or setting), so default-fill or skip missing fields on read \u2014 never assume a loaded record has every field the current code expects. Bump the key version when the shape changes.\n- Lift state to App when multiple children need it; keep it local otherwise. Extract non-trivial logic into custom hooks (useTodos, useTimer).\n- useReducer for state shapes with several interrelated fields (cards + columns + filters + drag state).\n\nFILE & IMAGE UPLOAD:\n- Picker: `<input type=\"file\" accept=\"image/*\" multiple>` (vary accept by use case: `.csv,text/csv`, `.txt,.md,text/plain`, etc.).\n- Drag-and-drop: call `e.preventDefault()` in onDragOver (otherwise drop never fires); read files from `event.dataTransfer.files`. Drop and picker should coexist.\n- Previews: `<img src={URL.createObjectURL(file)} />`. Call `URL.revokeObjectURL` on unmount or before replacing \u2014 otherwise the browser leaks memory.\n- localStorage cannot hold large binary. Persist only metadata for big files; re-upload on next visit (or use IndexedDB if you really need durable binary storage).\n- Validate size and type before processing; reject files larger than the app can hold in memory.\n\nAI CAPABILITIES \u2014 `window.app.complete(prompt: string): Promise<string>` is available in this environment. Build for the real call.\n- When the app's core purpose is conversation, generation, grading, recommendation, or any natural-language task, this IS the implementation \u2014 call it. NEVER fake it: no hardcoded or canned replies, no random or `setTimeout` \"thinking\", no demo-mode stub. A chat app, tutor, or assistant that doesn't call `window.app.complete` is wrong, not a placeholder.\n- Don't force it where it isn't needed \u2014 a calculator, todo list, or timer has no reason to call it. Match the implementation to what the app actually does.\n- Async: wrap in try/catch, show a loading state, and disable dependent inputs while waiting. Surface failures gracefully (\"Couldn't reach the AI \u2014 try again\").\n- Structure prompts with clear sections (Role / Context / Task / Format) and pass the relevant state (e.g. the running conversation) as context. Render the response as untrusted text: `white-space: pre-wrap`, never dangerouslySetInnerHTML.\n- Skip the call only when the answer is genuinely static \u2014 no language or reasoning needed. Cache repeated prompts in component state when sensible.\n\nCODE QUALITY \u2014 write as a senior engineer would:\n- Handle edge cases (empty inputs, division by zero, invalid data). Show helpful error states, not crashes.\n- Never let formatting throw during render: guard `new Date(x)` / `Intl` / `toLocaleString` / `.toFixed` against missing or invalid values (e.g. `Number.isNaN(d.getTime())`) and return a fallback \u2014 one bad value from storage or state must not blank the whole app.\n- Semantic HTML: real headings, labels, buttons (not divs with onClick), form elements.\n- Accessible: aria-labels on icon buttons, focus-visible styles, keyboard navigable.\n- Extract into custom hooks or helpers when a component grows past ~80 lines.\n\nSTYLING \u2014 Tailwind is the CHASSIS, custom CSS carries the IDENTITY:\n- Tailwind (via Play CDN) for layout, spacing, state (hover/focus), responsive prefixes \u2014 uniform structure.\n- App.css carries the identity: @import the Google Fonts, declare CSS custom properties (--bg, --panel, --card, --ink, --muted, --accent, --hairline, --radius, --radius-sm, --shadow-card, --shadow-card-hi) in :root, gradients/textures/signature animations Tailwind can't express. Reference tokens from Tailwind via arbitrary values (`className=\"bg-[var(--bg)]\"`) when needed. A well-designed app has 60-200 lines of App.css \u2014 not zero.\n- Modern CSS is how apps stop reading as generic-AI-output: `color-mix(in oklab, var(--accent) 14%, transparent)` for tinting, `oklch()` / `oklab()` for perceptually-uniform color, `:has()`, `conic-gradient` / `radial-gradient`, `backdrop-filter`, `@container` queries, inline SVG `feTurbulence` for texture.\n- Don't pull additional CSS frameworks. Tailwind + custom CSS is enough.\n\nVISUAL DESIGN \u2014 every app should look like a real product, not a wireframe:\n- Let the chosen aesthetic drive every decision. \"Editorial\" wants italic display type and restrained color; \"retro arcade\" wants chunky shadows and saturated gradients. No safe-modern defaults.\n- Atmospheric depth: subtle radial wash, paper grain (inline SVG noise), warm/cool tint \u2014 every canvas should feel like a designed space, not a #fff rectangle.\n- Three shadow tiers (rest / hover / active). Pair drop shadows with inset highlights (`box-shadow: 0 1px 0 rgba(255,255,255,.9) inset, 0 6px 14px -10px ...`) for tactile feel.\n- Typography hierarchy: display font for headlines (italic accents on key words via `<em>`), body font for content, mono for metadata/labels \u2014 different families telegraph different information types.\n- Interactive polish: hover/active/focus on every clickable element, 200-300ms cubic-bezier transitions, keyboard shortcuts where natural (Esc to cancel, Cmd/Ctrl+Enter to commit), focus-visible rings keyed to the accent.\n- Light/dark mode if reasonable: a single CSS variable inversion under `[data-theme=\"dark\"]`.\n- Customization affordance for multi-session apps: small \"Tweaks\" panel with theme / density / accent. Skip for throwaway demos.\n- Responsive: Tailwind responsive prefixes (`sm:`, `md:`, `lg:`) and `@media` in App.css. Mobile-first.";
1521
1526
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -145,7 +145,7 @@
145
145
  "homepage": "https://github.com/anuma-ai/sdk#readme",
146
146
  "packageManager": "pnpm@10.23.0",
147
147
  "dependencies": {
148
- "@anuma/portal": "1.0.0-next.20260715224031",
148
+ "@anuma/portal": "1.0.0-next.20260717201251",
149
149
  "@babel/parser": "^7.29.2",
150
150
  "@babel/types": "^7.29.0",
151
151
  "ai": "6.0.202",