@chibitek/chibi 0.1.173 → 0.1.175

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.
Files changed (83) hide show
  1. package/dist/assets/{abnfDiagram-O67JEVCF-DaA9WpZw.js → abnfDiagram-O67JEVCF-CjXTIXBz.js} +1 -1
  2. package/dist/assets/{arc-DJ4Kdvv6.js → arc-1QgG2iO9.js} +1 -1
  3. package/dist/assets/{architectureDiagram-NJMV4G6O-B975BUTv.js → architectureDiagram-NJMV4G6O-CzCo4hRJ.js} +1 -1
  4. package/dist/assets/{blockDiagram-BEXU5L5S-CXOOfLy7.js → blockDiagram-BEXU5L5S-DhrHIUjL.js} +1 -1
  5. package/dist/assets/{c4Diagram-YGBWAQC7-CorqQ94E.js → c4Diagram-YGBWAQC7-gOjFoY7Z.js} +1 -1
  6. package/dist/assets/channel-DB0DMkPD.js +1 -0
  7. package/dist/assets/{chunk-2Q5K7J3B-CPFHpKez.js → chunk-2Q5K7J3B-B9bsMZTt.js} +1 -1
  8. package/dist/assets/{chunk-3FUC2YCW-CrzBuB3A.js → chunk-3FUC2YCW-D6qIOfLe.js} +2 -2
  9. package/dist/assets/{chunk-5VM5RSS4-DXJTBdvO.js → chunk-5VM5RSS4-DCixOiOi.js} +1 -1
  10. package/dist/assets/{chunk-GTNCS2PH-B0TxALG4.js → chunk-GTNCS2PH-Bh52Az-G.js} +1 -1
  11. package/dist/assets/{chunk-GWA4HPMP-BL44uVOP.js → chunk-GWA4HPMP-Dbs4EZ88.js} +1 -1
  12. package/dist/assets/{chunk-JWPE2WC7-CxSklH7P.js → chunk-JWPE2WC7-jReoeHz2.js} +1 -1
  13. package/dist/assets/{chunk-NETBCI7D-_Lb-D8AY.js → chunk-NETBCI7D-CF3H0lZo.js} +1 -1
  14. package/dist/assets/{chunk-WEXAMYUT-Dz5jp0xb.js → chunk-WEXAMYUT-DetCJmJX.js} +1 -1
  15. package/dist/assets/{chunk-XXDRQBXY-C-cd9N0r.js → chunk-XXDRQBXY-CigsN-8U.js} +1 -1
  16. package/dist/assets/{classDiagram-v2-NBCMYWYE-DtQ1781-.js → classDiagram-v2-NBCMYWYE-D8ZcMUWM.js} +1 -1
  17. package/dist/assets/{cose-bilkent-JH36ORCC-CHzWXNdH.js → cose-bilkent-JH36ORCC-BnqFoYIa.js} +1 -1
  18. package/dist/assets/{cynefin-OW5HDTMX-lHbKcf1S.js → cynefin-OW5HDTMX-DWSSGPqP.js} +1 -1
  19. package/dist/assets/{cynefinDiagram-VND7K2PF-BxY6MCKD.js → cynefinDiagram-VND7K2PF-Doo-ZQ5E.js} +1 -1
  20. package/dist/assets/{dagre-6A5THRUB-C2SBw-Mo.js → dagre-6A5THRUB-Bje_N-ue.js} +1 -1
  21. package/dist/assets/{diagram-22UHCM2B-BwIAlmAU.js → diagram-22UHCM2B-osEfxfGj.js} +1 -1
  22. package/dist/assets/{diagram-3UASUU5V-i6fBkckD.js → diagram-3UASUU5V-D0ybINw1.js} +1 -1
  23. package/dist/assets/{diagram-ATOU4E4O-DwedCbB_.js → diagram-ATOU4E4O-DwS59waF.js} +1 -1
  24. package/dist/assets/{diagram-CDSNMT55-BISF-AQC.js → diagram-CDSNMT55-stDTabsA.js} +1 -1
  25. package/dist/assets/{diagram-MLGK6HIB-ImgdRRxp.js → diagram-MLGK6HIB-COdtBBj4.js} +1 -1
  26. package/dist/assets/{diagram-MPIPVDR6-CxlAwPCS.js → diagram-MPIPVDR6-S32r0-4f.js} +1 -1
  27. package/dist/assets/{ebnfDiagram-ZINNZB2B-sVJkymKa.js → ebnfDiagram-ZINNZB2B-CZxbMOzV.js} +1 -1
  28. package/dist/assets/{elk-276RUBZZ-DU1vyX4w.js → elk-276RUBZZ-PzYOam0u.js} +1 -1
  29. package/dist/assets/{erDiagram-OPXOYQCR-CnCmL1Ve.js → erDiagram-OPXOYQCR-D0HBOTqF.js} +1 -1
  30. package/dist/assets/{flowDiagram-KWPJA3E3-DME2uLJf.js → flowDiagram-KWPJA3E3-1d8rftXL.js} +1 -1
  31. package/dist/assets/{ganttDiagram-FUAMR5RP-CJQNW9ts.js → ganttDiagram-FUAMR5RP-CWn83VOd.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-X574FWY7-D88_Y9tg.js → gitGraphDiagram-X574FWY7-CkLyRxvr.js} +1 -1
  33. package/dist/assets/{index-U9LN4GhF.js → index-BcBNl1Sc.js} +175 -175
  34. package/dist/assets/index-D17LLrYN.css +1 -0
  35. package/dist/assets/{index-ChmZZe7i.js → index-DfdDXEfq.js} +1 -1
  36. package/dist/assets/{infoDiagram-VRGFBTTK-CxWEf3NI.js → infoDiagram-VRGFBTTK-BmAsjOaa.js} +1 -1
  37. package/dist/assets/{ishikawaDiagram-OU5B5YK6-B_YoNVjF.js → ishikawaDiagram-OU5B5YK6-B9d57hp_.js} +1 -1
  38. package/dist/assets/{journeyDiagram-ZHPQQLJL-ZYuBE2Bm.js → journeyDiagram-ZHPQQLJL-II3Kse3X.js} +1 -1
  39. package/dist/assets/{kanban-definition-PNTS6WVX-jmY_M4Qf.js → kanban-definition-PNTS6WVX-DtovCoCx.js} +1 -1
  40. package/dist/assets/{linear-kKUF6ayN.js → linear-BnH8vI1-.js} +1 -1
  41. package/dist/assets/{mermaid.core-DC5051S_.js → mermaid.core-BExpGmRM.js} +5 -5
  42. package/dist/assets/{mindmap-definition-NLK3R4M7-C5knfygJ.js → mindmap-definition-NLK3R4M7-CSGJyaor.js} +1 -1
  43. package/dist/assets/{pegDiagram-GJSIUBJH-Ex6iedL2.js → pegDiagram-GJSIUBJH-BFgKWA7i.js} +1 -1
  44. package/dist/assets/{pieDiagram-5QR66LMP-Dv38Ox0D.js → pieDiagram-5QR66LMP-Dl3FAvDB.js} +1 -1
  45. package/dist/assets/{quadrantDiagram-O4NWA36T-CwOEor8N.js → quadrantDiagram-O4NWA36T-DafRWmMB.js} +1 -1
  46. package/dist/assets/{railroadDiagram-XR7U4H2S-DVgqz5MR.js → railroadDiagram-XR7U4H2S-CHJ7SyyJ.js} +1 -1
  47. package/dist/assets/{requirementDiagram-PLB6GJNP-BxiuYo52.js → requirementDiagram-PLB6GJNP-DJzpaN8z.js} +1 -1
  48. package/dist/assets/{sankeyDiagram-IPEJSGJF--vq_1flP.js → sankeyDiagram-IPEJSGJF-BKqi0qaK.js} +1 -1
  49. package/dist/assets/{sequenceDiagram-PO4LG4MO-9hLpfPA6.js → sequenceDiagram-PO4LG4MO-Bdc2LKBx.js} +1 -1
  50. package/dist/assets/{sizeCapture-INFHLROL-BeGMkj9s.js → sizeCapture-INFHLROL-CIuw2Jav.js} +1 -1
  51. package/dist/assets/{stateDiagram-v2-GCMORJYK-BnNOMugb.js → stateDiagram-v2-GCMORJYK-CocMnCmt.js} +1 -1
  52. package/dist/assets/{swimlanes-2SLR337P-C1YG6Jq-.js → swimlanes-2SLR337P-D7Kw4SfT.js} +1 -1
  53. package/dist/assets/swimlanesDiagram-TC7HE7FX-Bq7WUTaZ.js +8 -0
  54. package/dist/assets/{timeline-definition-EJHVYXUP-Co7OtwGA.js → timeline-definition-EJHVYXUP-DEXnhdui.js} +1 -1
  55. package/dist/assets/{usecaseDiagram-POWQR4AR-BYHFARv3.js → usecaseDiagram-POWQR4AR-CJTa0bWM.js} +1 -1
  56. package/dist/assets/{vennDiagram-UO4OBE2U-C-sdhL3h.js → vennDiagram-UO4OBE2U-CKphfio6.js} +1 -1
  57. package/dist/assets/{wardleyDiagram-VNRHLVJA-wf8ZNrmQ.js → wardleyDiagram-VNRHLVJA-COQ5I4oG.js} +1 -1
  58. package/dist/assets/{xychartDiagram-PMCCYNJV-BjRlkVGv.js → xychartDiagram-PMCCYNJV-BqlgNUIm.js} +1 -1
  59. package/dist/index.html +2 -2
  60. package/dist-server/container-mcp.js +1 -0
  61. package/dist-server/index.js +1305 -747
  62. package/dist-server/openmausbot.js +3 -2
  63. package/dist-server/pair-cli.js +3 -2
  64. package/dist-server/server/about-me-sync.js +172 -0
  65. package/dist-server/server/config.js +1 -1
  66. package/dist-server/server/dictation.js +8 -0
  67. package/dist-server/server/drivers/claude.js +4 -4
  68. package/dist-server/server/icm-context.js +0 -22
  69. package/dist-server/server/index.js +26 -31
  70. package/dist-server/server/oauth-account-core.js +153 -0
  71. package/dist-server/server/oauth-account-tools.js +7 -348
  72. package/dist-server/server/oauth-calendar-tools.js +551 -0
  73. package/dist-server/server/paw/service.js +2 -1
  74. package/dist-server/server/paw/workspace.js +2 -1
  75. package/dist-server/server/vault/routes.js +20 -0
  76. package/dist-server/server/vault/store.js +24 -0
  77. package/dist-server/shared/oauth-providers.js +2 -1
  78. package/dist-server/shared/vault-csv.js +94 -0
  79. package/dist-server/vps-container-mcp.js +1 -0
  80. package/package.json +1 -1
  81. package/dist/assets/channel-sBYHnjxz.js +0 -1
  82. package/dist/assets/index-COwCaN7p.css +0 -1
  83. package/dist/assets/swimlanesDiagram-TC7HE7FX-9DghUs7S.js +0 -8
@@ -20900,6 +20900,7 @@ var init_config = __esm({
20900
20900
  icm: external_exports.object({
20901
20901
  enabled: external_exports.boolean().optional(),
20902
20902
  aboutMePrefilled: external_exports.boolean().optional(),
20903
+ // no longer read (About me syncs with PAW); kept so older configs parse
20903
20904
  path: external_exports.string().trim().max(4096).refine(
20904
20905
  (value) => !["\r", "\n", "\0"].some((character) => value.includes(character)),
20905
20906
  "Use a folder path without control characters"
@@ -30179,8 +30180,8 @@ var init_claude = __esm({
30179
30180
  DEFAULT_AUTOCOMPACT_TOKENS = 2e5;
30180
30181
  CLAUDE_FLAG_FLOORS = {
30181
30182
  "--strict-mcp-config": [1, 0, 60],
30182
- "--setting-sources": [1, 0, 122],
30183
- "--autocompact": [2, 1, 122],
30183
+ "--setting-sources": [1, 0, 125],
30184
+ "--autocompact": [2, 1, 221],
30184
30185
  // 2.1.267 is the first CLI that accepts it; below that the recorded prompt
30185
30186
  // simply is not refreshed, which is the pre-existing behaviour.
30186
30187
  "--system-prompt-snapshot": [2, 1, 267],
@@ -20900,6 +20900,7 @@ var init_config = __esm({
20900
20900
  icm: external_exports.object({
20901
20901
  enabled: external_exports.boolean().optional(),
20902
20902
  aboutMePrefilled: external_exports.boolean().optional(),
20903
+ // no longer read (About me syncs with PAW); kept so older configs parse
20903
20904
  path: external_exports.string().trim().max(4096).refine(
20904
20905
  (value) => !["\r", "\n", "\0"].some((character) => value.includes(character)),
20905
20906
  "Use a folder path without control characters"
@@ -30179,8 +30180,8 @@ var init_claude = __esm({
30179
30180
  DEFAULT_AUTOCOMPACT_TOKENS = 2e5;
30180
30181
  CLAUDE_FLAG_FLOORS = {
30181
30182
  "--strict-mcp-config": [1, 0, 60],
30182
- "--setting-sources": [1, 0, 122],
30183
- "--autocompact": [2, 1, 122],
30183
+ "--setting-sources": [1, 0, 125],
30184
+ "--autocompact": [2, 1, 221],
30184
30185
  // 2.1.267 is the first CLI that accepts it; below that the recorded prompt
30185
30186
  // simply is not refreshed, which is the pre-existing behaviour.
30186
30187
  "--system-prompt-snapshot": [2, 1, 267],
@@ -0,0 +1,172 @@
1
+ // About me lives in up to three places: Atomic's profile (every bot, cloud
2
+ // runs too), PAW me/about-me.md (local bots and CLI tools) and the person's
3
+ // Nexus about_me (every AI signed in to Nexus). This keeps them the same:
4
+ // an empty side is filled, an edit on one side reaches the others, and two
5
+ // sides that disagree wait for the person to pick one in Settings > About me.
6
+ import { readFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { writeFileAtomic } from "./atomic.js";
9
+ export const ABOUT_ME_SIDES = ["atomic", "paw", "nexus"];
10
+ const norm = (text) => text.replace(/\r\n?/g, "\n").trim();
11
+ /** One pass. `now` holds each reachable side's text (a side left out is
12
+ * skipped and caught up later); `seen` is what each side held after the
13
+ * last pass. A side whose text moved since then was edited, and a single
14
+ * edit reaches every other side. Two different edits, or a side seen for
15
+ * the first time that disagrees with the rest, are a conflict: nothing is
16
+ * written. `legacy` (an ICM "Who I am") fills everything when all sides are
17
+ * empty, and a profile still holding it counts as empty. */
18
+ export function reconcileAboutMe(now, seen, legacy = "") {
19
+ const sides = ABOUT_ME_SIDES.filter((side) => now[side] !== undefined);
20
+ const text = (side) => (side === "atomic" && norm(legacy) && norm(now.atomic) === norm(legacy) ? "" : norm(now[side]));
21
+ const filled = sides.filter((side) => text(side));
22
+ const edited = filled.filter((side) => text(side) !== norm(seen[side] ?? ""));
23
+ const kept = filled.filter((side) => !edited.includes(side));
24
+ const firstContact = edited.some((side) => seen[side] === undefined) && kept.some((side) => text(side) !== text(edited[0]));
25
+ if (new Set(edited.map(text)).size > 1 || firstContact)
26
+ return { writes: {}, seen, conflict: filled };
27
+ const source = edited[0] ?? kept[0];
28
+ const target = source ? now[source] : legacy;
29
+ if (!norm(target))
30
+ return { writes: {}, seen, conflict: [] };
31
+ const writes = {};
32
+ for (const side of sides)
33
+ if (norm(now[side]) !== norm(target))
34
+ writes[side] = target;
35
+ return { writes, seen: { ...seen, ...Object.fromEntries(sides.map((side) => [side, norm(target)])) }, conflict: [] };
36
+ }
37
+ /** The person's About me in Nexus (about_me_get / about_me_set). */
38
+ export function nexusAboutMe(client) {
39
+ if (!client)
40
+ return null;
41
+ return {
42
+ get: async () => {
43
+ const body = await client.call("about_me_get", {});
44
+ return typeof body.about_me === "string" ? body.about_me : "";
45
+ },
46
+ set: async (content) => { await client.call("about_me_set", { content }); },
47
+ };
48
+ }
49
+ /** How often a config read looks at Nexus again. */
50
+ const NEXUS_EVERY_MS = 5 * 60_000;
51
+ export class AboutMeSync {
52
+ deps;
53
+ path;
54
+ seen;
55
+ conflicts = {};
56
+ nexusAt = 0;
57
+ running = null;
58
+ constructor(deps) {
59
+ this.deps = deps;
60
+ this.path = join(deps.dataDir, "about-me-sync.json");
61
+ try {
62
+ const raw = JSON.parse(readFileSync(this.path, "utf8"));
63
+ this.seen = Object.fromEntries(ABOUT_ME_SIDES.filter((side) => typeof raw[side] === "string").map((side) => [side, raw[side]]));
64
+ }
65
+ catch {
66
+ this.seen = {};
67
+ }
68
+ }
69
+ /** The texts that disagree, by side, for Settings > About me. Empty when they agree. */
70
+ conflict() {
71
+ return { ...this.conflicts };
72
+ }
73
+ /** Atomic and PAW now, then Nexus; the app hears each time About me changes. */
74
+ sync(maxAgeMs) {
75
+ if (this.local())
76
+ this.deps.changed();
77
+ void this.all(maxAgeMs).then((changed) => changed && this.deps.changed());
78
+ }
79
+ /** Atomic and PAW, now. True when Atomic's About me changed. Never throws. */
80
+ local() {
81
+ const before = this.deps.profile.get();
82
+ try {
83
+ this.pass({ atomic: before, ...(this.deps.paw() ? { paw: this.deps.paw().get() } : {}) });
84
+ }
85
+ catch (error) {
86
+ console.warn(`[about-me] could not sync with PAW: ${error.message}`);
87
+ }
88
+ return this.deps.profile.get() !== before;
89
+ }
90
+ /** Every side, Nexus included. A config read passes the default and looks
91
+ * at Nexus at most every five minutes; a save passes 0. Never throws. */
92
+ async all(maxAgeMs = NEXUS_EVERY_MS) {
93
+ const clock = this.deps.now ?? Date.now;
94
+ if (this.running)
95
+ return this.running;
96
+ if (!this.deps.nexus() || clock() - this.nexusAt < maxAgeMs)
97
+ return this.local();
98
+ this.nexusAt = clock();
99
+ this.running = (async () => {
100
+ const nexus = this.deps.nexus();
101
+ const before = this.deps.profile.get();
102
+ let text;
103
+ try {
104
+ text = await nexus.get();
105
+ }
106
+ catch (error) {
107
+ console.warn(`[about-me] could not read Nexus: ${error.message}`);
108
+ this.local();
109
+ return this.deps.profile.get() !== before;
110
+ }
111
+ try {
112
+ const write = this.pass({ atomic: before, ...(this.deps.paw() ? { paw: this.deps.paw().get() } : {}), nexus: text });
113
+ if (write !== undefined) {
114
+ await nexus.set(write);
115
+ this.save({ ...this.seen, nexus: norm(write) });
116
+ }
117
+ }
118
+ catch (error) {
119
+ console.warn(`[about-me] could not sync with Nexus: ${error.message}`);
120
+ }
121
+ return this.deps.profile.get() !== before;
122
+ })().finally(() => { this.running = null; });
123
+ return this.running;
124
+ }
125
+ /** The person picked `side` in a conflict: its text goes everywhere.
126
+ * False when `side` is not one of the texts that disagree. */
127
+ async resolve(side) {
128
+ const text = ABOUT_ME_SIDES.includes(side) ? this.conflicts[side] : undefined;
129
+ if (text === undefined)
130
+ return false;
131
+ this.conflicts = {};
132
+ const paw = this.deps.paw();
133
+ if (norm(this.deps.profile.get()) !== norm(text))
134
+ this.deps.profile.set(text);
135
+ if (paw && norm(paw.get()) !== norm(text))
136
+ paw.set(text);
137
+ this.save({ ...this.seen, atomic: norm(text), ...(paw ? { paw: norm(text) } : {}) });
138
+ const nexus = this.deps.nexus();
139
+ if (nexus) {
140
+ await nexus.set(text);
141
+ this.save({ ...this.seen, nexus: norm(text) });
142
+ }
143
+ this.deps.changed();
144
+ return true;
145
+ }
146
+ /** Writes Atomic and PAW; returns the text Nexus should get, if any. */
147
+ pass(now) {
148
+ const result = reconcileAboutMe(now, this.seen, this.deps.legacy());
149
+ // a pass that could not see Nexus keeps a Nexus conflict on screen
150
+ this.conflicts = result.conflict.length || now.nexus !== undefined || !this.conflicts.nexus
151
+ ? Object.fromEntries(result.conflict.map((side) => [side, now[side]]))
152
+ : this.conflicts;
153
+ if (result.writes.paw !== undefined)
154
+ this.deps.paw()?.set(result.writes.paw);
155
+ if (result.writes.atomic !== undefined)
156
+ this.deps.profile.set(result.writes.atomic);
157
+ // Nexus counts as seen only once the write lands
158
+ this.save(result.writes.nexus === undefined ? result.seen : { ...result.seen, nexus: this.seen.nexus });
159
+ return result.writes.nexus;
160
+ }
161
+ save(next) {
162
+ if (JSON.stringify(next) === JSON.stringify(this.seen))
163
+ return;
164
+ this.seen = next;
165
+ try {
166
+ writeFileAtomic(this.path, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
167
+ }
168
+ catch (error) {
169
+ console.warn(`[about-me] could not save sync state: ${error.message}`);
170
+ }
171
+ }
172
+ }
@@ -470,7 +470,7 @@ const appConfigSchema = z.object({
470
470
  * On unless `enabled` is false; an empty or absent path is ~/workspace/ICM. */
471
471
  icm: z.object({
472
472
  enabled: z.boolean().optional(),
473
- aboutMePrefilled: z.boolean().optional(),
473
+ aboutMePrefilled: z.boolean().optional(), // no longer read (About me syncs with PAW); kept so older configs parse
474
474
  path: z.string().trim().max(4096).refine((value) => !["\r", "\n", "\0"].some((character) => value.includes(character)), "Use a folder path without control characters").optional(),
475
475
  }).strict().optional(),
476
476
  /** Settings > Memory (server/nexus-memory): always write session outcomes,
@@ -84,6 +84,14 @@ export function checkCleanup(input, output) {
84
84
  if (tokens.filter((t) => kept.has(t)).length / tokens.length < 0.6)
85
85
  return { ok: false, reason: "overlap" };
86
86
  }
87
+ // a correction swaps a misheard word for the right one; words with nothing
88
+ // taken out for them were never said
89
+ const spoken = new Set(contentTokens(input));
90
+ const written = new Set(contentTokens(cleaned));
91
+ const added = [...written].filter((t) => !spoken.has(t)).length;
92
+ const removed = [...spoken].filter((t) => !written.has(t)).length;
93
+ if (added > removed + 1)
94
+ return { ok: false, reason: "added" };
87
95
  return { ok: true, text: cleaned };
88
96
  }
89
97
  export async function ask(provider, prompt, timeoutMs) {
@@ -259,12 +259,12 @@ const DEFAULT_AUTOCOMPACT_TOKENS = 200_000;
259
259
  *
260
260
  * Verified against the published binaries, not the changelog (which never
261
261
  * records `--autocompact`): `--strict-mcp-config` is present in 1.0.60 and
262
- * absent from 1.0.0; `--setting-sources` first appears in 1.0.122 (1.0.120
263
- * lacks it); `--autocompact` first appears in 2.1.122 (2.1.121 lacks it). */
262
+ * absent from 1.0.0; `--setting-sources` first appears in 1.0.125 (1.0.124
263
+ * lacks it); `--autocompact` first appears in 2.1.221 (2.1.220 lacks it). */
264
264
  export const CLAUDE_FLAG_FLOORS = {
265
265
  "--strict-mcp-config": [1, 0, 60],
266
- "--setting-sources": [1, 0, 122],
267
- "--autocompact": [2, 1, 122],
266
+ "--setting-sources": [1, 0, 125],
267
+ "--autocompact": [2, 1, 221],
268
268
  // 2.1.267 is the first CLI that accepts it; below that the recorded prompt
269
269
  // simply is not refreshed, which is the pre-existing behaviour.
270
270
  "--system-prompt-snapshot": [2, 1, 267],
@@ -145,25 +145,3 @@ export function icmAboutMeSummary(folder) {
145
145
  }
146
146
  return "";
147
147
  }
148
- /** The one-time config patch that fills an empty About me from Layer 0, or
149
- * null when there is nothing to do. An About me the person already wrote is
150
- * never replaced: the patch then only records that the check happened, so a
151
- * later edit, or clearing the field on purpose, is never refilled. */
152
- export function icmAboutMePrefill(cfg, home = homedir()) {
153
- if (!icmEnabled(cfg) || cfg.icm?.aboutMePrefilled)
154
- return null;
155
- const folder = resolveIcmPath(cfg, home);
156
- if (!icmFolderPresent(folder))
157
- return null;
158
- if (cfg.profile?.aboutMe?.trim())
159
- return { icm: { aboutMePrefilled: true } };
160
- const aboutMe = icmAboutMeSummary(folder);
161
- return aboutMe ? { profile: { aboutMe }, icm: { aboutMePrefilled: true } } : null;
162
- }
163
- /** About me still holds exactly what was filled in from ICM. */
164
- export function aboutMeFromIcm(cfg, home = homedir()) {
165
- const current = cfg.profile?.aboutMe?.trim();
166
- if (!cfg.icm?.aboutMePrefilled || !current)
167
- return false;
168
- return current === icmAboutMeSummary(resolveIcmPath(cfg, home));
169
- }
@@ -214,7 +214,8 @@ import { RTK_LAUNCHER_PATH, rtkLocalStatus } from "./rtk.js";
214
214
  import { withoutRtkLauncher } from "./rtk-rewrite.js";
215
215
  import { loadAddonSkillLibraries } from "./skill-addons.js";
216
216
  import { browserSkillCapabilities, loadBundledSkills, loadUserSkills, mergeSkills, renderSkillInstructions, selectBundledSkills } from "./skill-library.js";
217
- import { aboutMeFromIcm, icmAboutMePrefill, icmStatus, icmSystemPrompt } from "./icm-context.js";
217
+ import { AboutMeSync, nexusAboutMe } from "./about-me-sync.js";
218
+ import { icmAboutMeSummary, icmEnabled, icmStatus, icmSystemPrompt, resolveIcmPath } from "./icm-context.js";
218
219
  import { PawService } from "./paw/service.js";
219
220
  import { chibiFilingPrompt, parseFiledTaskId } from "./paw/change-requests.js";
220
221
  import { installedPlaybookInstructions } from "./installed-playbooks.js";
@@ -506,22 +507,6 @@ ensureNodeShim();
506
507
  // Extract the bundled npm (Resources/npm/npm-<version>.tgz) once, soon after
507
508
  // boot, so the first "Connect Claude" does not wait for it. It never throws.
508
509
  setTimeout(() => void bundledNpmCli(), 2_000).unref?.();
509
- /** Fill an empty About me from ICM Layer 0, once. True when config changed. */
510
- function prefillAboutMeFromIcm() {
511
- const patch = icmAboutMePrefill(cfg);
512
- if (!patch)
513
- return false;
514
- try {
515
- saveConfig(patch);
516
- Object.assign(cfg, loadConfig());
517
- return true;
518
- }
519
- catch (error) {
520
- console.warn(`[icm] could not fill About me: ${error.message}`);
521
- return false;
522
- }
523
- }
524
- prefillAboutMeFromIcm();
525
510
  // Who asked for the turn running (or last run) on each thread, read by the
526
511
  // usage ledger when it settles. It is set when a turn is ADMITTED, from the
527
512
  // message that started it — never when a message merely queues behind a
@@ -2468,9 +2453,15 @@ try {
2468
2453
  catch (error) {
2469
2454
  console.warn(`[paw] could not create the workspace: ${error.message}`);
2470
2455
  }
2471
- function pawConfigStatus() {
2472
- return paw.summary();
2473
- }
2456
+ const aboutMe = new AboutMeSync({
2457
+ dataDir: DATA_DIR,
2458
+ profile: { get: () => cfg.profile?.aboutMe ?? "", set: (text) => { saveConfig({ profile: { aboutMe: text.slice(0, 24_000) } }); Object.assign(cfg, loadConfig()); } },
2459
+ paw: () => (paw.exists() ? { get: () => paw.aboutMe(), set: (text) => paw.setAboutMe(text) } : null),
2460
+ nexus: () => nexusAboutMe(nexusMemory.getClient()),
2461
+ legacy: () => (icmEnabled(cfg) ? icmAboutMeSummary(resolveIcmPath(cfg)) : ""),
2462
+ changed: () => broadcast({ kind: "config", ...configStatus() }),
2463
+ });
2464
+ void aboutMe.all(0);
2474
2465
  /** The PAW section of a turn's system prompt. A cloud run cannot reach the
2475
2466
  * folder, so it gets none. */
2476
2467
  function pawPrompt(bot, request, runOn) {
@@ -14458,11 +14449,11 @@ function configStatus() {
14458
14449
  imageGen: avatarImageStatus(cfg),
14459
14450
  // not a secret — the sidebar shows it
14460
14451
  workspaceName: cfg.workspaceName ?? "",
14461
- profile: { name: cfg.profile?.name ?? "", email: cfg.profile?.email ?? "", aboutMe: cfg.profile?.aboutMe ?? "" },
14452
+ profile: { name: cfg.profile?.name ?? "", email: cfg.profile?.email ?? "", aboutMe: cfg.profile?.aboutMe ?? "", aboutMeConflict: aboutMe.conflict() },
14462
14453
  // a folder path and whether it is there; nothing from inside it
14463
- icm: { ...icmStatus(cfg), aboutMeFromIcm: aboutMeFromIcm(cfg) },
14454
+ icm: icmStatus(cfg),
14464
14455
  // where PAW lives and whether the interview ran; the files stay on disk
14465
- paw: pawConfigStatus(),
14456
+ paw: paw.summary(),
14466
14457
  // the enrolled organisation's read-only desktop policy; null when not enrolled
14467
14458
  managedPolicy: managedPolicy.summary(),
14468
14459
  // whether this person sees Watch-Along (server/watch-along/access.ts)
@@ -14533,7 +14524,7 @@ function configForAccess(status, admin) {
14533
14524
  signIn: { admins: [], members: [] },
14534
14525
  vps: { configured: status.vps.configured, sshAlias: "" },
14535
14526
  profile: { name: status.profile.name, email: "" },
14536
- icm: { enabled: status.icm.enabled, found: status.icm.found, exists: status.icm.exists, path: "", defaultPath: "", aboutMeFromIcm: false },
14527
+ icm: { enabled: status.icm.enabled, found: status.icm.found, exists: status.icm.exists, path: "", defaultPath: "" },
14537
14528
  paw: { ...status.paw, root: "" },
14538
14529
  browserProfiles: status.browserProfiles.map((profile) => Object.fromEntries(Object.entries(profile).filter(([key]) => key !== "partitionId"))),
14539
14530
  };
@@ -24245,6 +24236,7 @@ const handleRequest = async (req, res) => {
24245
24236
  name: field("name", 80), role: field("role", 120), department: field("department", 80), company: field("company", 120),
24246
24237
  goals: field("goals", 2_000), workingStyle: field("workingStyle", 2_000),
24247
24238
  }, { chibitek: body?.chibitek === true });
24239
+ aboutMe.sync(0);
24248
24240
  broadcast({ kind: "config", ...configStatus() });
24249
24241
  return json(res, 200, { ...status, aboutMe: paw.aboutMe() });
24250
24242
  }
@@ -24255,8 +24247,13 @@ const handleRequest = async (req, res) => {
24255
24247
  if (!paw.exists())
24256
24248
  await paw.ensure({ chibitek: cfg.onboarding?.setup?.chibitek === true });
24257
24249
  paw.setAboutMe(body.text);
24250
+ aboutMe.sync(0);
24258
24251
  return json(res, 200, { ...paw.status(), aboutMe: paw.aboutMe() });
24259
24252
  }
24253
+ if (method === "POST" && path === "/api/paw/about-me/resolve") {
24254
+ const side = (await readBody(req))?.side;
24255
+ return await aboutMe.resolve(String(side)) ? json(res, 200, { ok: true }) : json(res, 409, { error: "Nothing to choose: About me already agrees." });
24256
+ }
24260
24257
  // Bot settings > Memory > Workspace knowledge
24261
24258
  if (method === "GET" && path === "/api/paw/suggestable") {
24262
24259
  return json(res, 200, { files: paw.suggestable() });
@@ -24289,10 +24286,8 @@ const handleRequest = async (req, res) => {
24289
24286
  return json(res, 404, { error: "not found" });
24290
24287
  }
24291
24288
  if (method === "GET" && path === "/api/config") {
24292
- // an ICM folder created while the server runs (the setup chat) fills an
24293
- // empty About me the next time Settings or the app asks
24294
- if (prefillAboutMeFromIcm())
24295
- broadcast({ kind: "config", ...configStatus() });
24289
+ // a me/about-me.md or Nexus edit, or a new ICM folder, reaches About me on a read
24290
+ aboutMe.sync();
24296
24291
  return json(res, 200, configForAccess(configStatus(), auth.scopes.includes("admin")));
24297
24292
  }
24298
24293
  if ((method === "PUT" || method === "PATCH") && path === "/api/config") {
@@ -24698,9 +24693,9 @@ const handleRequest = async (req, res) => {
24698
24693
  syncCredentialEnv(patch);
24699
24694
  Object.assign(cfg, loadConfig());
24700
24695
  }
24701
- // a newly chosen or re-enabled ICM folder can fill an empty About me
24702
- if (patch.icm)
24703
- prefillAboutMeFromIcm();
24696
+ // an About me edit reaches PAW; a newly chosen ICM folder can fill an empty one
24697
+ if (patch.icm || patch.profile?.aboutMe !== undefined)
24698
+ aboutMe.sync(0);
24704
24699
  }
24705
24700
  catch (error) {
24706
24701
  if (configWriteCommitted) {
@@ -0,0 +1,153 @@
1
+ // What every account tool shares: the account and call types, argument
2
+ // basics, and one authenticated request to Google or Microsoft Graph.
3
+ import { z } from "zod";
4
+ import { OAUTH_PROVIDERS } from "../shared/oauth-providers.js";
5
+ const HTTP_TIMEOUT_MS = 30_000;
6
+ const MAX_RESULT_CHARS = 48_000;
7
+ const MAX_JSON_BYTES = 4 * 1024 * 1024;
8
+ export const MAX_DOWNLOAD_BYTES = 8 * 1024 * 1024;
9
+ export const READS = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true };
10
+ export const CREATES = { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true };
11
+ /** Sends mail or invitations to other people: cannot be taken back. */
12
+ export const SENDS = { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true };
13
+ export const accountArg = { type: "string", description: "Email address of the account to use. Optional only when this bot has exactly one account of this provider." };
14
+ export const EMAIL = /^[^\s@<>,;"]+@[^\s@<>,;"]+\.[^\s@<>,;"]+$/;
15
+ export const noNewlines = (value) => !/[\r\n]/.test(value);
16
+ export const base = z.object({ account: z.string().max(320).optional() });
17
+ export class ToolInputError extends Error {
18
+ }
19
+ export function bounded(text, limit = MAX_RESULT_CHARS) {
20
+ return text.length <= limit ? text : `${text.slice(0, limit)}\n[truncated: ask for less]`;
21
+ }
22
+ export function addresses(value, field) {
23
+ if (value === undefined)
24
+ return [];
25
+ const list = (Array.isArray(value) ? value : value.split(",")).map((item) => item.trim()).filter(Boolean);
26
+ if (list.length > 50)
27
+ throw new ToolInputError(`${field}: at most 50 addresses`);
28
+ for (const item of list) {
29
+ if (!EMAIL.test(item))
30
+ throw new ToolInputError(`${field}: "${item.slice(0, 80)}" is not an email address`);
31
+ }
32
+ return list;
33
+ }
34
+ export class ProviderApiError extends Error {
35
+ status;
36
+ constructor(status, message) {
37
+ super(message);
38
+ this.status = status;
39
+ }
40
+ }
41
+ export async function readLimited(response, limit) {
42
+ const declared = Number(response.headers.get("content-length") ?? "0");
43
+ if (declared > limit) {
44
+ await response.body?.cancel().catch(() => { });
45
+ return null;
46
+ }
47
+ if (!response.body)
48
+ return Buffer.alloc(0);
49
+ const reader = response.body.getReader();
50
+ const chunks = [];
51
+ let size = 0;
52
+ for (;;) {
53
+ const { done, value } = await reader.read();
54
+ if (done)
55
+ break;
56
+ size += value.byteLength;
57
+ if (size > limit) {
58
+ await reader.cancel().catch(() => { });
59
+ return null;
60
+ }
61
+ chunks.push(value);
62
+ }
63
+ return Buffer.concat(chunks);
64
+ }
65
+ export function providerErrorMessage(body, status) {
66
+ const error = body?.error;
67
+ const message = typeof error === "string"
68
+ ? error
69
+ : error && typeof error === "object" && typeof error.message === "string"
70
+ ? error.message
71
+ : "";
72
+ return (message || `request failed with HTTP ${status}`).split(/\r?\n/)[0].slice(0, 300);
73
+ }
74
+ export async function call(ctx, account, url, init) {
75
+ const fetcher = ctx.fetch ?? fetch;
76
+ const { expect, ...request } = init;
77
+ for (let attempt = 0; attempt < 2; attempt += 1) {
78
+ const token = await ctx.accessToken(account.id);
79
+ const headers = new Headers(request.headers);
80
+ headers.set("authorization", `Bearer ${token}`);
81
+ if (!headers.has("accept") && expect === "json")
82
+ headers.set("accept", "application/json");
83
+ const response = await fetcher(url, { ...request, headers, signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) });
84
+ if (response.status === 401 && attempt === 0) {
85
+ await response.body?.cancel().catch(() => { });
86
+ ctx.invalidate(account.id);
87
+ continue;
88
+ }
89
+ if (!response.ok) {
90
+ const raw = await readLimited(response, 64 * 1024);
91
+ let body = null;
92
+ try {
93
+ body = raw ? JSON.parse(raw.toString("utf8")) : null;
94
+ }
95
+ catch {
96
+ body = null;
97
+ }
98
+ const message = providerErrorMessage(body, response.status);
99
+ if (response.status === 403)
100
+ throw new ProviderApiError(403, `${account.email} does not allow this: ${message}`);
101
+ if (response.status === 404)
102
+ throw new ProviderApiError(404, `Not found in ${account.email}: ${message}`);
103
+ throw new ProviderApiError(response.status, `${OAUTH_PROVIDERS[account.provider].label} refused the request for ${account.email}: ${message}`);
104
+ }
105
+ if (expect === "none") {
106
+ await response.body?.cancel().catch(() => { });
107
+ return undefined;
108
+ }
109
+ const bytes = await readLimited(response, expect === "json" ? MAX_JSON_BYTES : MAX_DOWNLOAD_BYTES);
110
+ if (expect === "bytes")
111
+ return bytes;
112
+ if (!bytes)
113
+ throw new ProviderApiError(502, "The response was too large");
114
+ try {
115
+ return JSON.parse(bytes.toString("utf8"));
116
+ }
117
+ catch {
118
+ throw new ProviderApiError(502, "The service returned an unreadable response");
119
+ }
120
+ }
121
+ throw new ProviderApiError(401, `${account.email} needs to sign in again: open Settings → Accounts and connect it again.`);
122
+ }
123
+ export function parse(schema, value, what) {
124
+ const parsed = schema.safeParse(value);
125
+ if (!parsed.success)
126
+ throw new ProviderApiError(502, `Unexpected ${what} response`);
127
+ return parsed.data;
128
+ }
129
+ export function json(value) {
130
+ return JSON.stringify(value, null, 1);
131
+ }
132
+ export function htmlToText(html) {
133
+ return html
134
+ .replace(/<(script|style)[\s\S]*?<\/\1>/gi, "")
135
+ .replace(/<br\s*\/?>/gi, "\n")
136
+ .replace(/<\/(p|div|tr|li|h\d)>/gi, "\n")
137
+ .replace(/<[^>]+>/g, "")
138
+ .replace(/&nbsp;/g, " ")
139
+ .replace(/&amp;/g, "&")
140
+ .replace(/&lt;/g, "<")
141
+ .replace(/&gt;/g, ">")
142
+ .replace(/&quot;/g, '"')
143
+ .replace(/&#39;/g, "'")
144
+ .replace(/\n{3,}/g, "\n\n")
145
+ .trim();
146
+ }
147
+ export const GRAPH = "https://graph.microsoft.com/v1.0";
148
+ export const graphRecipient = z.object({ emailAddress: z.object({ address: z.string().optional(), name: z.string().optional() }).optional() });
149
+ export function recipient(value) {
150
+ const address = value?.emailAddress?.address;
151
+ const name = value?.emailAddress?.name;
152
+ return address ? (name && name !== address ? `${name} <${address}>` : address) : undefined;
153
+ }