@chibitek/chibi 0.1.174 → 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.
- package/dist/assets/{abnfDiagram-O67JEVCF-BVDndX1g.js → abnfDiagram-O67JEVCF-CjXTIXBz.js} +1 -1
- package/dist/assets/{arc-WduS6wYB.js → arc-1QgG2iO9.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-CYK9LkCf.js → architectureDiagram-NJMV4G6O-CzCo4hRJ.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-BMj-aPW9.js → blockDiagram-BEXU5L5S-DhrHIUjL.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-cXYIStgL.js → c4Diagram-YGBWAQC7-gOjFoY7Z.js} +1 -1
- package/dist/assets/channel-DB0DMkPD.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-CZ_nCppA.js → chunk-2Q5K7J3B-B9bsMZTt.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-C3wRAbtM.js → chunk-3FUC2YCW-D6qIOfLe.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-CMvgOXJP.js → chunk-5VM5RSS4-DCixOiOi.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-9wBuLQzU.js → chunk-GTNCS2PH-Bh52Az-G.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-BpJMmfAi.js → chunk-GWA4HPMP-Dbs4EZ88.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-BnM06ABv.js → chunk-JWPE2WC7-jReoeHz2.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-CjjiiXiT.js → chunk-NETBCI7D-CF3H0lZo.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-D3heHHZs.js → chunk-WEXAMYUT-DetCJmJX.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-n_P41ZlM.js → chunk-XXDRQBXY-CigsN-8U.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-Dz60Ek-6.js → classDiagram-v2-NBCMYWYE-D8ZcMUWM.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-BCt6vBi9.js → cose-bilkent-JH36ORCC-BnqFoYIa.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-U0uq4EAQ.js → cynefin-OW5HDTMX-DWSSGPqP.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-CaQX0eUJ.js → cynefinDiagram-VND7K2PF-Doo-ZQ5E.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DlCMESYH.js → dagre-6A5THRUB-Bje_N-ue.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-Cl1xmlTD.js → diagram-22UHCM2B-osEfxfGj.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-Cus3CPg7.js → diagram-3UASUU5V-D0ybINw1.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-i06rMvae.js → diagram-ATOU4E4O-DwS59waF.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-CUM5_heN.js → diagram-CDSNMT55-stDTabsA.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-B5NKRruo.js → diagram-MLGK6HIB-COdtBBj4.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-CAumAcGl.js → diagram-MPIPVDR6-S32r0-4f.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-B19d7d8l.js → ebnfDiagram-ZINNZB2B-CZxbMOzV.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-NJEpfRWi.js → elk-276RUBZZ-PzYOam0u.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-BExk6CHc.js → erDiagram-OPXOYQCR-D0HBOTqF.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-ClExQ_p0.js → flowDiagram-KWPJA3E3-1d8rftXL.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-1MeM9sSl.js → ganttDiagram-FUAMR5RP-CWn83VOd.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-Tw9Dudpa.js → gitGraphDiagram-X574FWY7-CkLyRxvr.js} +1 -1
- package/dist/assets/{index-KYHldywn.js → index-BcBNl1Sc.js} +175 -175
- package/dist/assets/index-D17LLrYN.css +1 -0
- package/dist/assets/{index-qsorLr2x.js → index-DfdDXEfq.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-ChU2tjvl.js → infoDiagram-VRGFBTTK-BmAsjOaa.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-BeTbrX2Q.js → ishikawaDiagram-OU5B5YK6-B9d57hp_.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-B7ijaWBD.js → journeyDiagram-ZHPQQLJL-II3Kse3X.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-JBmK2F1r.js → kanban-definition-PNTS6WVX-DtovCoCx.js} +1 -1
- package/dist/assets/{linear-B7FXe3Ue.js → linear-BnH8vI1-.js} +1 -1
- package/dist/assets/{mermaid.core-BPlfZpqv.js → mermaid.core-BExpGmRM.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-CmT3S5QR.js → mindmap-definition-NLK3R4M7-CSGJyaor.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-CFFHIcdQ.js → pegDiagram-GJSIUBJH-BFgKWA7i.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-yTiPmXRV.js → pieDiagram-5QR66LMP-Dl3FAvDB.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-Cw8u7esm.js → quadrantDiagram-O4NWA36T-DafRWmMB.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-BfKggsOW.js → railroadDiagram-XR7U4H2S-CHJ7SyyJ.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-Cf3n79jF.js → requirementDiagram-PLB6GJNP-DJzpaN8z.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-B2NkSZig.js → sankeyDiagram-IPEJSGJF-BKqi0qaK.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-BfFYiYK3.js → sequenceDiagram-PO4LG4MO-Bdc2LKBx.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-CZ_2nC3f.js → sizeCapture-INFHLROL-CIuw2Jav.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-B9jwzMXf.js → stateDiagram-v2-GCMORJYK-CocMnCmt.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-Bxhafu77.js → swimlanes-2SLR337P-D7Kw4SfT.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-Bq7WUTaZ.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-Dle5SJls.js → timeline-definition-EJHVYXUP-DEXnhdui.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-CgRTu9EO.js → usecaseDiagram-POWQR4AR-CJTa0bWM.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-D7m_1z6B.js → vennDiagram-UO4OBE2U-CKphfio6.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-7-TzmX4L.js → wardleyDiagram-VNRHLVJA-COQ5I4oG.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BVXXdviS.js → xychartDiagram-PMCCYNJV-BqlgNUIm.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +1 -0
- package/dist-server/index.js +660 -420
- package/dist-server/openmausbot.js +1 -0
- package/dist-server/pair-cli.js +1 -0
- package/dist-server/server/about-me-sync.js +172 -0
- package/dist-server/server/config.js +1 -1
- package/dist-server/server/icm-context.js +0 -22
- package/dist-server/server/index.js +26 -31
- package/dist-server/server/paw/service.js +2 -1
- package/dist-server/server/paw/workspace.js +2 -1
- package/dist-server/server/vault/routes.js +20 -0
- package/dist-server/server/vault/store.js +24 -0
- package/dist-server/shared/vault-csv.js +94 -0
- package/dist-server/vps-container-mcp.js +1 -0
- package/package.json +1 -1
- package/dist/assets/channel-TratrwmM.js +0 -1
- package/dist/assets/index-COwCaN7p.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-DWNGCaH3.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"
|
package/dist-server/pair-cli.js
CHANGED
|
@@ -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"
|
|
@@ -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,
|
|
@@ -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 {
|
|
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
|
-
|
|
2472
|
-
|
|
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:
|
|
14454
|
+
icm: icmStatus(cfg),
|
|
14464
14455
|
// where PAW lives and whether the interview ran; the files stay on disk
|
|
14465
|
-
paw:
|
|
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: ""
|
|
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
|
-
//
|
|
24293
|
-
|
|
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
|
|
24702
|
-
if (patch.icm)
|
|
24703
|
-
|
|
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) {
|
|
@@ -89,7 +89,8 @@ export class PawService {
|
|
|
89
89
|
this.store.setBaseline(path, readText(this.root, path) ?? "");
|
|
90
90
|
this.state = { ...this.state, template: "chibitek" };
|
|
91
91
|
}
|
|
92
|
-
|
|
92
|
+
// an About me already there (written by hand, or filled from Nexus) is kept
|
|
93
|
+
const result = applyInterview(this.root, { ...answers, name: answers.name || this.options.person().name }, { personEmail: this.options.person().email, keepAboutMe: Boolean(this.aboutMe().trim()) });
|
|
93
94
|
this.saveState({ ...this.state, departments: result.departments, interviewedAt: Date.now() });
|
|
94
95
|
return this.status();
|
|
95
96
|
}
|
|
@@ -187,7 +187,8 @@ export function applyInterview(root, answers, options = {}) {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
const aboutMe = aboutMeMarkdown(answers, department?.name ?? "", options.now);
|
|
190
|
-
|
|
190
|
+
if (!options.keepAboutMe)
|
|
191
|
+
writeText(root, "me/about-me.md", aboutMe);
|
|
191
192
|
applyLocks(root, manifest, options.personEmail ?? "");
|
|
192
193
|
return { departments, aboutMe };
|
|
193
194
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PASS } from "../routes/table.js";
|
|
2
|
+
import { VaultCsvError, vaultLoginsFromCsv } from "../../shared/vault-csv.js";
|
|
2
3
|
import { VaultError } from "./store.js";
|
|
3
4
|
/** The owner is supplied by the authenticated host, never from request JSON.
|
|
4
5
|
* Authorization runs on reads too, and is checked again after reading a body. */
|
|
@@ -13,6 +14,25 @@ export function createVaultRoutes(deps) {
|
|
|
13
14
|
const store = deps.store();
|
|
14
15
|
if (!store)
|
|
15
16
|
return json(res, 503, { error: "Vault encryption is unavailable. Unlock the secure store and try again." });
|
|
17
|
+
if (path === "/api/vault/import") {
|
|
18
|
+
if (method !== "POST")
|
|
19
|
+
return json(res, 405, { error: "Method not allowed." });
|
|
20
|
+
const body = await readBody(req, 8_000_000).catch(() => null);
|
|
21
|
+
if (deps.owner(req, auth) !== ownerId || deps.store() !== store)
|
|
22
|
+
return json(res, 403, { error: "Vault access changed. Try again." });
|
|
23
|
+
if (typeof body?.csv !== "string")
|
|
24
|
+
return json(res, 400, { error: "Choose a CSV file exported from your password manager." });
|
|
25
|
+
try {
|
|
26
|
+
const { logins, skipped } = vaultLoginsFromCsv(body.csv);
|
|
27
|
+
return json(res, 200, { ...store.importLogins(ownerId, logins), skipped });
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// Neither message echoes anything from a row.
|
|
31
|
+
if (error instanceof VaultError || error instanceof VaultCsvError)
|
|
32
|
+
return json(res, 400, { error: error.message });
|
|
33
|
+
return json(res, 503, { error: "Vault could not complete the request." });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
16
36
|
const item = /^\/api\/vault\/([0-9a-f-]{36})$/.exec(path);
|
|
17
37
|
if (path !== "/api/vault" && !item)
|
|
18
38
|
return json(res, 404, { error: "Vault item is unavailable." });
|
|
@@ -143,6 +143,30 @@ export class VaultStore {
|
|
|
143
143
|
return vaultMetadata(item);
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
|
+
/** Adds logins in one write. A login already saved for the same website
|
|
147
|
+
* and username is left as it is and counted in `duplicates`. */
|
|
148
|
+
importLogins(ownerId, logins) {
|
|
149
|
+
owner(ownerId);
|
|
150
|
+
const parsed = logins.map((input) => vaultInputSchema.safeParse(input));
|
|
151
|
+
if (parsed.some((result) => !result.success))
|
|
152
|
+
throw new VaultError("Invalid Vault item.");
|
|
153
|
+
return this.mutate((items) => {
|
|
154
|
+
const key = (item) => item.kind === "login" ? `${item.website}\n${item.username}` : "";
|
|
155
|
+
const seen = new Set(items.filter((item) => item.ownerId === ownerId).map(key));
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
let added = 0;
|
|
158
|
+
for (const { data } of parsed) {
|
|
159
|
+
if (seen.has(key(data)))
|
|
160
|
+
continue;
|
|
161
|
+
seen.add(key(data));
|
|
162
|
+
items.push({ ...data, id: randomUUID(), ownerId, createdAt: now, updatedAt: now });
|
|
163
|
+
added++;
|
|
164
|
+
}
|
|
165
|
+
if (items.length > MAX_ITEMS)
|
|
166
|
+
throw new VaultError(`Vault holds up to ${MAX_ITEMS} items. This import would go over that.`);
|
|
167
|
+
return { added, duplicates: logins.length - added };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
146
170
|
update(ownerId, id, input) {
|
|
147
171
|
owner(ownerId);
|
|
148
172
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { vaultOrigin } from "./vault.js";
|
|
2
|
+
export class VaultCsvError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/** RFC 4180: quoted fields, doubled quotes, commas and newlines inside quotes. */
|
|
5
|
+
export function parseCsv(text) {
|
|
6
|
+
const rows = [];
|
|
7
|
+
let row = [];
|
|
8
|
+
let field = "";
|
|
9
|
+
let quoted = false;
|
|
10
|
+
const s = text.replace(/^/, "");
|
|
11
|
+
for (let i = 0; i < s.length; i++) {
|
|
12
|
+
const c = s[i];
|
|
13
|
+
if (quoted) {
|
|
14
|
+
if (c === '"' && s[i + 1] === '"') {
|
|
15
|
+
field += '"';
|
|
16
|
+
i++;
|
|
17
|
+
}
|
|
18
|
+
else if (c === '"')
|
|
19
|
+
quoted = false;
|
|
20
|
+
else
|
|
21
|
+
field += c;
|
|
22
|
+
}
|
|
23
|
+
else if (c === '"')
|
|
24
|
+
quoted = true;
|
|
25
|
+
else if (c === ",") {
|
|
26
|
+
row.push(field);
|
|
27
|
+
field = "";
|
|
28
|
+
}
|
|
29
|
+
else if (c === "\n" || c === "\r") {
|
|
30
|
+
if (c === "\r" && s[i + 1] === "\n")
|
|
31
|
+
i++;
|
|
32
|
+
row.push(field);
|
|
33
|
+
rows.push(row);
|
|
34
|
+
row = [];
|
|
35
|
+
field = "";
|
|
36
|
+
}
|
|
37
|
+
else
|
|
38
|
+
field += c;
|
|
39
|
+
}
|
|
40
|
+
if (field || row.length) {
|
|
41
|
+
row.push(field);
|
|
42
|
+
rows.push(row);
|
|
43
|
+
}
|
|
44
|
+
return rows.filter((r) => r.some((cell) => cell.trim()));
|
|
45
|
+
}
|
|
46
|
+
// Header names used by Chrome, Edge, Safari/Apple Passwords, Firefox,
|
|
47
|
+
// 1Password, Bitwarden, LastPass, Dashlane, Proton Pass, NordPass, RoboForm.
|
|
48
|
+
// Earlier names win, so a "username" column beats an "email" column.
|
|
49
|
+
const COLUMNS = {
|
|
50
|
+
url: ["url", "login_uri", "website", "web site", "uri", "login url"],
|
|
51
|
+
username: ["username", "login_username", "user name", "login", "user", "login name", "email", "e-mail"],
|
|
52
|
+
password: ["password", "login_password", "pwd", "pass"],
|
|
53
|
+
name: ["name", "title", "account"],
|
|
54
|
+
type: ["type"],
|
|
55
|
+
};
|
|
56
|
+
/** Logins from a password manager's CSV export. Notes, TOTP seeds and custom
|
|
57
|
+
* fields are dropped: they can hold secrets and hints are not secret. Rows
|
|
58
|
+
* without a website, username and password, or that are not logins, are
|
|
59
|
+
* counted in `skipped`. Imported items are private to the person (no bots). */
|
|
60
|
+
export function vaultLoginsFromCsv(text) {
|
|
61
|
+
const [header, ...rows] = parseCsv(text);
|
|
62
|
+
if (!header)
|
|
63
|
+
return { logins: [], skipped: 0 };
|
|
64
|
+
const names = header.map((cell) => cell.trim().toLowerCase());
|
|
65
|
+
const col = (key) => {
|
|
66
|
+
for (const name of COLUMNS[key]) {
|
|
67
|
+
const index = names.indexOf(name);
|
|
68
|
+
if (index >= 0)
|
|
69
|
+
return index;
|
|
70
|
+
}
|
|
71
|
+
return -1;
|
|
72
|
+
};
|
|
73
|
+
const at = { url: col("url"), username: col("username"), password: col("password"), name: col("name"), type: col("type") };
|
|
74
|
+
if (at.url < 0 || at.password < 0 || at.username < 0)
|
|
75
|
+
throw new VaultCsvError("This CSV has no website, username and password columns.");
|
|
76
|
+
const logins = [];
|
|
77
|
+
let skipped = 0;
|
|
78
|
+
for (const row of rows) {
|
|
79
|
+
const cell = (index) => (index >= 0 ? row[index] ?? "" : "").trim();
|
|
80
|
+
const type = cell(at.type).toLowerCase();
|
|
81
|
+
const raw = cell(at.url).split(/[\s,]+/)[0] ?? "";
|
|
82
|
+
// Old entries saved as http:// or a bare domain are filled over HTTPS.
|
|
83
|
+
const website = vaultOrigin(/^[a-z][a-z0-9+.-]*:\/\//i.test(raw) ? raw.replace(/^http:\/\//i, "https://") : `https://${raw}`);
|
|
84
|
+
const username = cell(at.username);
|
|
85
|
+
const password = row[at.password] ?? "";
|
|
86
|
+
if ((type && type !== "login") || !raw || !website || !username || !password || username.length > 320 || password.length > 4096) {
|
|
87
|
+
skipped++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const label = (cell(at.name) || new URL(website).hostname).slice(0, 200);
|
|
91
|
+
logins.push({ kind: "login", label, hints: "", botIds: [], expiresAt: null, website, username, password });
|
|
92
|
+
}
|
|
93
|
+
return { logins, skipped };
|
|
94
|
+
}
|
|
@@ -17055,6 +17055,7 @@ var appConfigSchema = external_exports.object({
|
|
|
17055
17055
|
icm: external_exports.object({
|
|
17056
17056
|
enabled: external_exports.boolean().optional(),
|
|
17057
17057
|
aboutMePrefilled: external_exports.boolean().optional(),
|
|
17058
|
+
// no longer read (About me syncs with PAW); kept so older configs parse
|
|
17058
17059
|
path: external_exports.string().trim().max(4096).refine(
|
|
17059
17060
|
(value) => !["\r", "\n", "\0"].some((character) => value.includes(character)),
|
|
17060
17061
|
"Use a folder path without control characters"
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{U as a,C as n}from"./mermaid.core-BPlfZpqv.js";const t=(r,o)=>a.lang.round(n.parse(r)[o]);export{t as c};
|