memhouse 0.18.12 → 0.20.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.
- package/AGENTS.md +22 -151
- package/CHANGELOG.md +263 -29
- package/LICENSE +202 -22
- package/LICENSES/agentlytics-MIT.txt +21 -0
- package/LICENSES/memhouse-MIT-until-0.18.11.txt +22 -0
- package/LICENSES/models.dev-MIT.txt +22 -0
- package/NOTICE +28 -0
- package/README.md +42 -20
- package/SECURITY.md +21 -8
- package/bin/memhouse.js +2223 -311
- package/docs/README.md +11 -9
- package/docs/guides/admin-password.md +37 -10
- package/docs/guides/configuration.md +13 -6
- package/docs/guides/daemons.md +64 -3
- package/docs/guides/import-claude-ai.md +204 -0
- package/docs/guides/install.md +11 -6
- package/docs/guides/invite.md +21 -7
- package/docs/guides/operate.md +114 -7
- package/docs/guides/search-and-resume.md +72 -3
- package/docs/guides/share.md +91 -52
- package/docs/guides/storage.md +44 -4
- package/docs/guides/troubleshooting.md +121 -14
- package/docs/guides/uninstall.md +4 -3
- package/docs/guides/update.md +16 -0
- package/docs/guides/upgrade-to-0.20.md +188 -0
- package/docs/releases/README.md +2 -1
- package/docs/releases/v0.18.10.md +5 -5
- package/docs/releases/v0.18.11.md +3 -3
- package/docs/releases/v0.20.0.md +627 -0
- package/docs/releases/v0.20.1.md +114 -0
- package/docs/tutorials/01-first-house.md +10 -7
- package/docs/tutorials/02-join-a-house.md +24 -15
- package/docs/tutorials/03-share-your-memory.md +40 -31
- package/editors/antigravity.js +1 -80
- package/editors/base.js +463 -9
- package/editors/bridge.js +119 -0
- package/editors/claude.js +273 -105
- package/editors/codex.js +278 -52
- package/editors/copilot.js +1 -70
- package/editors/cursor.js +1 -75
- package/editors/derive-fingerprints.json +18 -0
- package/editors/harness-paths.js +69 -0
- package/editors/index.js +1 -20
- package/editors/normalise/agy.js +910 -0
- package/editors/normalise/copilot-cli.js +321 -0
- package/editors/normalise/cursor-agent.js +852 -0
- package/editors/normalise/cursor.js +474 -0
- package/editors/normalise/devin.js +575 -0
- package/editors/normalise/gemini-cli.js +602 -0
- package/editors/normalise/goose.js +673 -0
- package/editors/normalise/opencode.js +633 -0
- package/editors/normalise/vscode.js +549 -0
- package/editors/normalise/zed.js +473 -0
- package/editors/opencode-v4.js +119 -0
- package/editors/scope.js +14 -3
- package/editors/sessions-log.js +349 -0
- package/editors/sqlite.js +28 -1
- package/editors/vscode.js +1 -72
- package/editors/windsurf.js +1 -143
- package/examples/01-local-one-machine/README.md +24 -19
- package/examples/01-local-one-machine/run.sh +1 -1
- package/examples/02-existing-clickhouse/README.md +30 -22
- package/examples/02-existing-clickhouse/run.sh +5 -4
- package/examples/03-team-house/README.md +44 -37
- package/examples/03-team-house/run.sh +35 -18
- package/examples/04-self-hosted-team/README.md +29 -14
- package/examples/04-self-hosted-team/compose.yml +5 -2
- package/examples/04-self-hosted-team/smoke.sh +26 -12
- package/examples/README.md +8 -4
- package/examples/fixtures/README.md +4 -1
- package/examples/fixtures/make-sessions.js +8 -5
- package/memhouse/admin-secret.js +89 -24
- package/memhouse/argv-secrets.js +64 -0
- package/memhouse/channel.js +49 -9
- package/memhouse/codex-plugin.js +194 -0
- package/memhouse/delivery/AGENT-INSTALL-PROMPT.md +2 -1
- package/memhouse/delivery/AGENT-INSTALL.md +22 -11
- package/memhouse/delivery/INVITATION.md.tpl +44 -45
- package/memhouse/delivery/PROMPT.md +53 -5
- package/memhouse/delivery/UPGRADE.md.tpl +1 -1
- package/memhouse/delivery/codex/AGENTS-BLOCK.md +11 -0
- package/memhouse/delivery/codex/SKILL.md +50 -0
- package/memhouse/delivery/kernel-install.md +17 -9
- package/memhouse/delivery/plugin/.claude-plugin/plugin.json +2 -2
- package/memhouse/delivery/plugin/reference/HOUSE.md +168 -41
- package/memhouse/delivery/plugin/skills/access/SKILL.md +13 -5
- package/memhouse/delivery/plugin/skills/admin/SKILL.md +14 -6
- package/memhouse/delivery/plugin/skills/house/SKILL.md +25 -8
- package/memhouse/delivery/plugin/skills/recall/SKILL.md +66 -8
- package/memhouse/delivery/plugin/skills/sql/SKILL.md +66 -7
- package/memhouse/deploy.js +1 -1
- package/memhouse/flags.js +35 -18
- package/memhouse/help.js +37 -0
- package/memhouse/host.js +82 -2
- package/memhouse/house/HOUSE.md +2 -2
- package/memhouse/house/convert-profile.js +638 -0
- package/memhouse/house/convert-v3.js +742 -0
- package/memhouse/house/doctor-v3.js +324 -0
- package/memhouse/house/host-alias.js +85 -0
- package/memhouse/house/house.js +3 -3
- package/memhouse/house/migrate.js +2 -2
- package/memhouse/house/profile-dedupe.js +183 -0
- package/memhouse/house/profile-edges.js +554 -0
- package/memhouse/house/profile-house.js +430 -0
- package/memhouse/house/profile-rollup.js +190 -0
- package/memhouse/house/q-cli.js +863 -0
- package/memhouse/house/queries-v3.js +1013 -0
- package/memhouse/house/reconcile-v3.js +192 -0
- package/memhouse/house/records-to-profile.js +174 -0
- package/memhouse/house/records-to-rows.js +169 -0
- package/memhouse/house/relocate.js +2 -1
- package/memhouse/house/schema-choice.js +121 -0
- package/memhouse/house/schema-profile.js +290 -0
- package/memhouse/house/schema-profile.sql.tpl +398 -0
- package/memhouse/house/schema-v3.js +267 -0
- package/memhouse/house/schema-v3.sql.tpl +213 -0
- package/memhouse/import/claude-ai.js +420 -0
- package/memhouse/import/import.js +372 -0
- package/memhouse/import/pyjson.js +206 -0
- package/memhouse/import/records.js +64 -0
- package/memhouse/import/writer-profile.js +192 -0
- package/memhouse/import/writer-v3.js +240 -0
- package/memhouse/import/zip.js +112 -0
- package/memhouse/invitation.js +102 -0
- package/memhouse/invitefile.js +5 -2
- package/memhouse/plugin-copies.js +176 -0
- package/memhouse/provision.js +7 -2
- package/memhouse/release-hint.js +24 -0
- package/memhouse/resume.js +4 -4
- package/memhouse/server/api-v3.js +632 -0
- package/memhouse/server/dashboard-auth.js +181 -0
- package/memhouse/server/dashboard-port.js +91 -0
- package/memhouse/server/queries.js +30 -0
- package/memhouse/server/server.js +118 -20
- package/memhouse/service.js +1 -1
- package/memhouse/share.js +687 -1
- package/memhouse/shipper/adapter-capture.js +756 -0
- package/memhouse/shipper/capture.js +2571 -0
- package/memhouse/shipper/dedupe-profile.js +237 -0
- package/memhouse/shipper/dedupe-v3.js +1027 -0
- package/memhouse/shipper/forks.js +233 -0
- package/memhouse/shipper/log-adapter.js +219 -0
- package/memhouse/shipper/maintenance.js +49 -0
- package/memhouse/shipper/normalise-run.js +148 -0
- package/memhouse/shipper/passlog.js +68 -5
- package/memhouse/shipper/profile-capture.js +903 -0
- package/memhouse/shipper/ship.js +91 -8
- package/memhouse/shipper/snapshots.js +193 -0
- package/memhouse/shipper/source-roots.js +111 -0
- package/memhouse/version-notice.js +66 -0
- package/package.json +14 -7
- package/public/THIRD_PARTY_NOTICES.txt +2 -2
- package/public/assets/index-CcNnhfM3.js +78 -0
- package/public/index.html +1 -1
- package/ui/package-lock.json +56 -37
- package/ui/package.json +1 -0
- package/ui/src/App.jsx +29 -14
- package/ui/src/components/ChatSidebar.jsx +69 -12
- package/ui/src/lib/api.js +76 -0
- package/ui/src/lib/dashboard-token.js +41 -0
- package/ui/src/main.jsx +22 -5
- package/ui/src/pages/Dashboard.jsx +22 -36
- package/ui/third-party-notices.js +119 -0
- package/ui/vite.config.js +5 -1
- package/docs/releases/v0.18.12.md +0 -59
- package/memhouse/COMPETITION.md +0 -221
- package/memhouse/DESIGN.md +0 -222
- package/memhouse/RESUME-AND-UPDATE.md +0 -182
- package/public/assets/index-CnWzrjF3.js +0 -78
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// The normalised session log (plan v4, docs/design/sessions-log.md). An adapter writes each session
|
|
3
|
+
// it reads as a log of sessions-model records (one JSON record per line), appended while the session
|
|
4
|
+
// only grows and rewritten when its history changed. The capture engine reads that log like any
|
|
5
|
+
// transcript, with this one generic derive for every harness.
|
|
6
|
+
//
|
|
7
|
+
// Layout: $MEMHOUSE_HOME/session-logs/<harness>/<encodeURIComponent(harness_path)>/<fileNameFor(...)>.jsonl
|
|
8
|
+
// (not logs/: that holds the daemons' own logs, shipper.log and the rest).
|
|
9
|
+
// — one directory per harness store; the engine takes harness_path from the directory name.
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
// Above every bridge derive version (editors/bridge.js): a harness that switches from the bridge to
|
|
15
|
+
// its normaliser keeps its session keys and file keys, so the engine sees an adapter upgrade, and the
|
|
16
|
+
// bridge-era rows (positional m<i> ids) are tombstoned as duplicates of the log's native ones.
|
|
17
|
+
// Every log derive version is at least LOG_VERSION_FLOOR; every bridge version is below it.
|
|
18
|
+
const LOG_VERSION_FLOOR = 1000;
|
|
19
|
+
const LOG_DERIVE_VERSION = 1006; // 1002: house token split, first-seen times by message_id; 1003: usage records, error/notice parts as events; 1004: the session's birth time (meta.created); 1005: the usage flags input_includes_cache / output_includes_reasoning, and the flags of a harness's unflagged logs; 1006: no derive change: comment wording in the shared capture region
|
|
20
|
+
const LOG_EXT = '.jsonl';
|
|
21
|
+
const LOGS_DIR = 'session-logs';
|
|
22
|
+
|
|
23
|
+
// ── running totals ────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
// Some stores keep a session's usage only as a running total, overwritten as the session grows (Zed's
|
|
25
|
+
// `cumulative_token_usage`). A normaliser hands it over as `{k: 'usage', running: true, usage_id:
|
|
26
|
+
// <series>, ts?, usage}`. Written as is, it would change in place on every turn, and every change
|
|
27
|
+
// rewrites the whole log. writeLog keeps it as increments instead: `{k: 'usage', usage_id:
|
|
28
|
+
// '<series>#<n>', delta_of: <series>, ts, model?, agent_id?, reason?, usage}`, one appended whenever
|
|
29
|
+
// the total has grown since the increments already in the log. The log only appends, and its
|
|
30
|
+
// increments sum to the store's total without overlapping (sessions-model §2.4 "No overlap"). An
|
|
31
|
+
// increment takes the running record's `ts` as it is when the increment is written: the store's own
|
|
32
|
+
// time for the total (Zed: the thread's last save), so the tokens are dated when they were spent, not
|
|
33
|
+
// when memhouse first read them. A running record without one gives `ts: null` (first seen).
|
|
34
|
+
const RUNNING_COUNTS = ['input', 'output', 'cache_read', 'cache_write', 'reasoning', 'tool', 'cost', 'credits'];
|
|
35
|
+
const isRunning = (r) => !!r && r.k === 'usage' && r.running === true;
|
|
36
|
+
const count0 = (v) => { const x = Number(v); return Number.isFinite(x) && x > 0 ? x : 0; };
|
|
37
|
+
/** An increment line already in the log, parsed; null for any other line. */
|
|
38
|
+
function incrementOf(line) {
|
|
39
|
+
if (!line.startsWith('{"k":"usage"')) return null;
|
|
40
|
+
try { const o = JSON.parse(line); return o && typeof o.delta_of === 'string' ? o : null; } catch { return null; }
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The increments to append so that, per series, the log's increments sum to its running total: [] when
|
|
44
|
+
* nothing grew, null when a total shrank below what the log already holds. A series with no running
|
|
45
|
+
* total this time keeps the increments it has.
|
|
46
|
+
*/
|
|
47
|
+
function increments(running, have) {
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const r of running) {
|
|
50
|
+
const series = String(r.usage_id);
|
|
51
|
+
const mine = have.filter((d) => d.delta_of === series);
|
|
52
|
+
const u = r.usage || {};
|
|
53
|
+
const grown = {};
|
|
54
|
+
let any = false;
|
|
55
|
+
for (const k of RUNNING_COUNTS) {
|
|
56
|
+
const total = count0(u[k]);
|
|
57
|
+
const had = mine.reduce((s, d) => s + count0(d.usage && d.usage[k]), 0);
|
|
58
|
+
if (total < had) return null;
|
|
59
|
+
grown[k] = total > had ? total - had : null;
|
|
60
|
+
if (grown[k] !== null) any = true;
|
|
61
|
+
}
|
|
62
|
+
if (!any) continue;
|
|
63
|
+
const fixed = Object.fromEntries(Object.entries(u).filter(([k]) => !RUNNING_COUNTS.includes(k)));
|
|
64
|
+
out.push({
|
|
65
|
+
k: 'usage', usage_id: `${series}#${mine.length + 1}`, delta_of: series, ts: Number(r.ts) > 0 ? Number(r.ts) : null,
|
|
66
|
+
...(r.model ? { model: r.model } : {}), ...(r.agent_id ? { agent_id: r.agent_id } : {}), ...(r.reason ? { reason: r.reason } : {}),
|
|
67
|
+
usage: { ...fixed, ...grown },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** The series whose running total fell below the increments the log already holds for it. */
|
|
73
|
+
function shrunk(running, have) {
|
|
74
|
+
const out = new Set();
|
|
75
|
+
for (const r of running) {
|
|
76
|
+
const series = String(r.usage_id);
|
|
77
|
+
const mine = have.filter((d) => d.delta_of === series);
|
|
78
|
+
const u = r.usage || {};
|
|
79
|
+
if (RUNNING_COUNTS.some((k) => count0(u[k]) < mine.reduce((s, d) => s + count0(d.usage && d.usage[k]), 0))) out.add(series);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Bring a session's log up to date: 'new' | 'append' | 'rewrite' | 'same'. Running totals among the
|
|
86
|
+
* records become increments (above). A rewrite keeps the increments the log holds, as they were, while
|
|
87
|
+
* the totals still cover them, so their rows keep their keys; a total that shrank starts its own
|
|
88
|
+
* series again from one increment, and every other series keeps its increments.
|
|
89
|
+
*/
|
|
90
|
+
function writeLog(file, records) {
|
|
91
|
+
const running = records.filter(isRunning);
|
|
92
|
+
const lines = records.filter((r) => !isRunning(r)).map((r) => JSON.stringify(r));
|
|
93
|
+
let old = null;
|
|
94
|
+
try { old = fs.readFileSync(file, 'utf-8').split('\n').filter(Boolean); } catch { old = null; }
|
|
95
|
+
const oldLines = []; const had = [];
|
|
96
|
+
for (const l of old || []) { const d = incrementOf(l); if (d) had.push({ line: l, rec: d }); else oldLines.push(l); }
|
|
97
|
+
if (old) {
|
|
98
|
+
let same = 0;
|
|
99
|
+
while (same < oldLines.length && same < lines.length && oldLines[same] === lines[same]) same++;
|
|
100
|
+
const grown = same === oldLines.length ? increments(running, had.map((d) => d.rec)) : null;
|
|
101
|
+
if (grown) {
|
|
102
|
+
const add = [...lines.slice(same), ...grown.map((r) => JSON.stringify(r))];
|
|
103
|
+
if (!add.length) return 'same';
|
|
104
|
+
fs.appendFileSync(file, add.map((l) => `${l}\n`).join(''));
|
|
105
|
+
return 'append';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
let kept = had;
|
|
109
|
+
let grown = increments(running, kept.map((d) => d.rec));
|
|
110
|
+
if (!grown) {
|
|
111
|
+
const restart = shrunk(running, had.map((d) => d.rec));
|
|
112
|
+
kept = had.filter((d) => !restart.has(d.rec.delta_of));
|
|
113
|
+
grown = increments(running, kept.map((d) => d.rec));
|
|
114
|
+
}
|
|
115
|
+
const all = [...lines, ...kept.map((d) => d.line), ...grown.map((r) => JSON.stringify(r))];
|
|
116
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
117
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
118
|
+
fs.writeFileSync(tmp, all.map((l) => `${l}\n`).join(''));
|
|
119
|
+
fs.renameSync(tmp, file);
|
|
120
|
+
return old ? 'rewrite' : 'new';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The directory of one harness store's logs. */
|
|
124
|
+
function logDir(home, harness, harnessPath) {
|
|
125
|
+
return path.join(home, LOGS_DIR, harness, encodeURIComponent(harnessPath));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* A session's file name: the URI-encoded id ('.' as %2E), then '@' and the encoded incarnation when
|
|
129
|
+
* there is one. encodeURIComponent always encodes '@', so a literal '@' only ever separates the two.
|
|
130
|
+
*/
|
|
131
|
+
function fileNameFor(sessionId, incarnation = '') {
|
|
132
|
+
const enc = (s) => encodeURIComponent(String(s)).replace(/\./g, '%2E');
|
|
133
|
+
return enc(sessionId) + (incarnation ? `@${enc(incarnation)}` : '') + LOG_EXT;
|
|
134
|
+
}
|
|
135
|
+
function parseFileName(name) {
|
|
136
|
+
const base = path.basename(name, LOG_EXT);
|
|
137
|
+
const at = base.indexOf('@');
|
|
138
|
+
try {
|
|
139
|
+
return at < 0 ? { session_id: decodeURIComponent(base), incarnation: '' }
|
|
140
|
+
: { session_id: decodeURIComponent(base.slice(0, at)), incarnation: decodeURIComponent(base.slice(at + 1)) };
|
|
141
|
+
} catch { return null; }
|
|
142
|
+
}
|
|
143
|
+
function sessionIdOf(name) { const p = parseFileName(name); return p ? p.session_id : null; }
|
|
144
|
+
|
|
145
|
+
// <derive> ====================================================================================
|
|
146
|
+
// The generic derive of every normalised log. Fingerprinted by misc/capture-test.js: change
|
|
147
|
+
// anything in this region and bump LOG_DERIVE_VERSION.
|
|
148
|
+
function n(v) { const x = Math.trunc(Number(v)); return Number.isFinite(x) && x > 0 ? x : 0; }
|
|
149
|
+
/**
|
|
150
|
+
* Tokens as the house stores them — the split Claude Code and Codex rows already use (the Anthropic
|
|
151
|
+
* API's): input WITHOUT cached input, output WITH reasoning, cache reads and writes on their own, so
|
|
152
|
+
* pricing charges each once. A record says what it counts with two flags (sessions-model §2.4):
|
|
153
|
+
* `input_includes_cache` true means `input` already counts the cache reads and writes — they come
|
|
154
|
+
* out of it (Gemini); `output_includes_reasoning` false means `output` leaves the reasoning out — it
|
|
155
|
+
* goes back in (Gemini, OpenCode v1). A flag that is not a boolean falls back to the record's
|
|
156
|
+
* `basis`: `net` subtracted cached input and reasoning output (OpenCode) — reasoning goes back into
|
|
157
|
+
* output; `gross` included both (prompt and completion as billed) — cache reads come out of input.
|
|
158
|
+
* `anthropic` is already the house split; `unknown`, like a null flag, is stored as given.
|
|
159
|
+
* A record with neither flag, in a harness's log, takes UNFLAGGED[harness] first.
|
|
160
|
+
*/
|
|
161
|
+
// Logs written before a normaliser set the flags (0.20 pre-releases): the flags its records carry
|
|
162
|
+
// now. Such a log outlives its source (normalise-run keeps the logs of sessions no store lists), so
|
|
163
|
+
// it is never written again and every re-derive reads it as it is.
|
|
164
|
+
const UNFLAGGED = { 'gemini-cli': { input_includes_cache: true, output_includes_reasoning: false } };
|
|
165
|
+
const isFlag = (v) => typeof v === 'boolean';
|
|
166
|
+
function houseTokens(u, harness = '') {
|
|
167
|
+
if (!u) return null;
|
|
168
|
+
if (UNFLAGGED[harness] && !isFlag(u.input_includes_cache) && !isFlag(u.output_includes_reasoning)) u = { ...u, ...UNFLAGGED[harness] };
|
|
169
|
+
const input = n(u.input); const cr = n(u.cache_read); const cw = n(u.cache_write); const rs = n(u.reasoning);
|
|
170
|
+
const inCache = u.input_includes_cache; const outReasoning = u.output_includes_reasoning;
|
|
171
|
+
let inTok = input;
|
|
172
|
+
if (inCache === true) inTok = Math.max(input - cr - cw, 0);
|
|
173
|
+
else if (inCache !== false && u.basis === 'gross') inTok = Math.max(input - cr, 0);
|
|
174
|
+
const addReasoning = outReasoning === false || (outReasoning !== true && u.basis === 'net');
|
|
175
|
+
return {
|
|
176
|
+
input_tokens: inTok,
|
|
177
|
+
output_tokens: n(u.output) + (addReasoning ? rs : 0),
|
|
178
|
+
cache_read_tokens: cr, cache_write_tokens: cw, reasoning_tokens: rs,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function logDeriveAdapter(harness) {
|
|
183
|
+
return {
|
|
184
|
+
harness,
|
|
185
|
+
deriveVersion: LOG_DERIVE_VERSION,
|
|
186
|
+
initState() { return { folder: '' }; },
|
|
187
|
+
unitStart(rec) { return !!rec.obj; },
|
|
188
|
+
track() {}, closeUnit() {}, openUnit() {}, endReplay() {},
|
|
189
|
+
process(d, rec) {
|
|
190
|
+
const o = rec.obj;
|
|
191
|
+
if (o.k === 'session') {
|
|
192
|
+
if (o.cwd) { d.a.folder = o.cwd; if (!d.c.meta.folder) d.c.meta.folder = o.cwd; }
|
|
193
|
+
if (o.title) d.c.meta.title = o.title;
|
|
194
|
+
if (o.version) { d.a.version = o.version; d.c.meta.version = o.version; }
|
|
195
|
+
// Birth time: tells a fork from its origin when the copies kept the origin's times (forks.js).
|
|
196
|
+
if (Number(o.created) > 0) d.c.meta.created = Number(o.created);
|
|
197
|
+
d.c.meta.links = Array.isArray(o.links) ? o.links : [];
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
// Usage the store records on its own, not on a message (Goose's ledger after /clear or a
|
|
201
|
+
// truncation, compaction calls): a kind 'usage' row, counted in tokens, never as a message.
|
|
202
|
+
if (o.k === 'usage') {
|
|
203
|
+
const tok = houseTokens(o.usage, harness);
|
|
204
|
+
if (!tok || !(tok.input_tokens || tok.output_tokens || tok.cache_read_tokens || tok.cache_write_tokens || tok.reasoning_tokens)) return;
|
|
205
|
+
const id = o.usage_id != null && o.usage_id !== '' ? `usage:${o.usage_id}` : `usage:@${rec.pos}`;
|
|
206
|
+
const row = d.turn(rec, { message_id: id, part_id: '', part_ord: 0, role: 'assistant', kind: 'usage', model: o.model || '', agent_id: o.agent_id || '',
|
|
207
|
+
ts: d.tsOf(rec, Number(o.ts), id), text: '', extra: o.reason ? { reason: String(o.reason) } : {} });
|
|
208
|
+
d.revise(row, tok, rec);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (o.k !== 'message') return;
|
|
212
|
+
// A message without its own time is dated once by its id, so a rewrite of the log keeps its key.
|
|
213
|
+
const ts = d.tsOf(rec, Number(o.ts), o.message_id);
|
|
214
|
+
const tok = houseTokens(o.usage, harness);
|
|
215
|
+
const base = {
|
|
216
|
+
message_id: String(o.message_id), role: o.role === 'user' ? 'user' : (o.role === 'system' ? 'system' : 'assistant'),
|
|
217
|
+
model: o.model || '', effort: o.effort || '', parent_id: o.parent_id || '', agent_id: o.agent_id || '',
|
|
218
|
+
state: o.state && o.state !== 'current' ? String(o.state) : '',
|
|
219
|
+
};
|
|
220
|
+
const extra = o.status && o.status !== 'complete' ? { status: o.status } : {};
|
|
221
|
+
let ord = 0;
|
|
222
|
+
let first = null;
|
|
223
|
+
for (const p of o.parts || []) {
|
|
224
|
+
const pts = d.tsOf(rec, Number(p.ts) || Number(o.ts), o.message_id);
|
|
225
|
+
const pid = String(p.part_id ?? ord);
|
|
226
|
+
if (p.kind === 'tool') {
|
|
227
|
+
const row = d.turn(rec, { ...base, part_id: `${pid}:call`, part_ord: ord++, kind: 'tool_call', ts: pts, text: `${p.tool} ${JSON.stringify(p.args ?? {})}`, extra });
|
|
228
|
+
first = first || row;
|
|
229
|
+
d.call(rec, { call_id: p.call_id, message_id: base.message_id, ts: pts, tool_name: p.tool, args: JSON.stringify(p.args ?? {}), model: base.model, agent_id: base.agent_id });
|
|
230
|
+
if (p.output != null || p.is_error) {
|
|
231
|
+
d.turn(rec, { ...base, role: 'user', part_id: `${pid}:result`, part_ord: ord++, kind: 'tool_result', ts: pts, text: String(p.output ?? ''), extra });
|
|
232
|
+
d.result(rec, p.call_id, { ts: pts, is_error: !!p.is_error, result_len: String(p.output ?? '').length });
|
|
233
|
+
}
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
// An error or notice part is an event of the message, never a message of its own.
|
|
237
|
+
const kind = { reasoning: 'thinking', summary: 'summary', file: 'attachment', error: 'event', notice: 'event' }[p.kind] || 'text';
|
|
238
|
+
const text = p.kind === 'file' ? `${p.media_type || 'file'} ${p.att_sha || p.ref || ''}`.trim() : String(p.text ?? '');
|
|
239
|
+
const fx = p.kind === 'file' ? { media_type: p.media_type, att_sha: p.att_sha, att_len: p.att_len } : (kind === 'event' ? { notice: p.kind } : {});
|
|
240
|
+
const row = d.turn(rec, { ...base, part_id: pid, part_ord: ord++, kind, ts: pts, text, extra: { ...extra, ...fx } });
|
|
241
|
+
first = first || row;
|
|
242
|
+
}
|
|
243
|
+
// A message with no parts keeps a header row: its status and usage anchor.
|
|
244
|
+
if (!first) first = d.turn(rec, { ...base, part_id: '', part_ord: 0, kind: 'none', ts, text: '', extra });
|
|
245
|
+
if (tok && (tok.input_tokens || tok.output_tokens || tok.cache_read_tokens || tok.cache_write_tokens || tok.reasoning_tokens)) d.revise(first, tok, rec);
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// </derive> ===================================================================================
|
|
251
|
+
|
|
252
|
+
/** The capture capability for one harness whose sessions are kept as normalised logs. */
|
|
253
|
+
function logCapture(harness) {
|
|
254
|
+
const adapter = logDeriveAdapter(harness);
|
|
255
|
+
return {
|
|
256
|
+
harness,
|
|
257
|
+
deriveVersion: LOG_DERIVE_VERSION,
|
|
258
|
+
sessions(root) {
|
|
259
|
+
let names = [];
|
|
260
|
+
try { names = fs.readdirSync(root.dir).filter((x) => x.endsWith(LOG_EXT)); } catch { return []; }
|
|
261
|
+
const out = [];
|
|
262
|
+
for (const x of names) {
|
|
263
|
+
const p = parseFileName(x);
|
|
264
|
+
if (p && p.session_id) out.push({ harness, session_id: p.session_id, incarnation: p.incarnation, root: root.name, paths: [path.join(root.dir, x)] });
|
|
265
|
+
}
|
|
266
|
+
return out;
|
|
267
|
+
},
|
|
268
|
+
sourceFiles(session) { return session.paths.map((p) => ({ file_key: path.basename(p), path: p, agent_id: '', role: 'main' })); },
|
|
269
|
+
attribution: () => ({ agent_type: '', agent_desc: '' }),
|
|
270
|
+
// Records the engine dates by their first sighting: a message, or a usage record (a running
|
|
271
|
+
// total's increment), with no time of its own. When a files row is lost, their first sightings
|
|
272
|
+
// are read back from the archive (restoreFirstSeen), by byte position in the generation the
|
|
273
|
+
// read starts at. That keeps their keys only while it is the generation they were first seen
|
|
274
|
+
// in: with the local cache lost too, the read starts at generation 0, and a log rewritten since
|
|
275
|
+
// has moved its records, so they are re-dated under new keys (a known issue, 0.20 release
|
|
276
|
+
// notes). An increment of a store that gives its total a time (Zed) carries it, and is not
|
|
277
|
+
// affected.
|
|
278
|
+
tsless: (o) => !o || ((o.k === 'message' || o.k === 'usage') && !(Number(o.ts) > 0)),
|
|
279
|
+
derive: (input) => require('./base').capture.runDerive(adapter, input),
|
|
280
|
+
adapter,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Capture roots for every store directory under $MEMHOUSE_HOME/logs, with their harness_path. */
|
|
285
|
+
function logRoots(home) {
|
|
286
|
+
const top = path.join(home, LOGS_DIR);
|
|
287
|
+
const roots = [];
|
|
288
|
+
let harnesses = [];
|
|
289
|
+
try { harnesses = fs.readdirSync(top).filter((h) => /^[a-z0-9][a-z0-9-]{0,40}$/.test(h)); } catch { return roots; }
|
|
290
|
+
for (const harness of harnesses) {
|
|
291
|
+
let stores = [];
|
|
292
|
+
try { stores = fs.readdirSync(path.join(top, harness)); } catch { continue; }
|
|
293
|
+
for (const enc of stores) {
|
|
294
|
+
let hp;
|
|
295
|
+
try { hp = decodeURIComponent(enc); } catch { continue; }
|
|
296
|
+
const dir = path.join(top, harness, enc);
|
|
297
|
+
try { if (!fs.statSync(dir).isDirectory()) continue; } catch { continue; }
|
|
298
|
+
roots.push({ harness, name: path.basename(hp) || 'default', dir, harness_path: hp });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return roots;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Sub-agents and forks a session started (the origin side of a link) go in the log as `child`
|
|
306
|
+
* records, `{k: 'child', kind, session_id, evidence, at?}`, each right after the message that names
|
|
307
|
+
* it, never on the session record. The session record is the log's first line: a child there
|
|
308
|
+
* changes it with every sub-agent spawned, and each change rewrites the whole log, re-archiving
|
|
309
|
+
* and re-emitting every row (misc/rewrite-freq.js measured a Zed thread with a sub-agent every five
|
|
310
|
+
* turns at 5.4x the archive of the same thread without). A normaliser returns its records through
|
|
311
|
+
* this, with `children` on the session record and, on each child, `after`: the id of the message
|
|
312
|
+
* it was found in, where it knows one.
|
|
313
|
+
* - A child whose message is not in the log (held back) waits for it.
|
|
314
|
+
* - One without `after` follows the first message whose parts name its session id.
|
|
315
|
+
* - One that no message names stays on the session record, and rewrites the log once, when it
|
|
316
|
+
* appears.
|
|
317
|
+
*/
|
|
318
|
+
function placeChildren(recs) {
|
|
319
|
+
if (!Array.isArray(recs) || !recs.length) return recs;
|
|
320
|
+
const head = recs[0];
|
|
321
|
+
if (!head || head.k !== 'session' || !('children' in head)) return recs;
|
|
322
|
+
const ids = new Set(recs.filter((r) => r && r.k === 'message').map((r) => String(r.message_id)));
|
|
323
|
+
const naming = (sid) => {
|
|
324
|
+
const m = recs.find((r) => r && r.k === 'message' && JSON.stringify(r.parts || []).includes(sid));
|
|
325
|
+
return m ? String(m.message_id) : null;
|
|
326
|
+
};
|
|
327
|
+
const byMsg = new Map(); const keep = [];
|
|
328
|
+
for (const c of Array.isArray(head.children) ? head.children : []) {
|
|
329
|
+
const { after, ...child } = c;
|
|
330
|
+
const m = after != null && after !== '' ? String(after) : naming(String(c.session_id));
|
|
331
|
+
if (m == null) { keep.push(child); continue; }
|
|
332
|
+
if (!ids.has(m)) continue;
|
|
333
|
+
if (!byMsg.has(m)) byMsg.set(m, []);
|
|
334
|
+
byMsg.get(m).push({ k: 'child', ...child });
|
|
335
|
+
}
|
|
336
|
+
const { children, ...rest } = head;
|
|
337
|
+
const out = [keep.length ? { ...rest, children: keep } : rest];
|
|
338
|
+
for (const r of recs.slice(1)) {
|
|
339
|
+
out.push(r);
|
|
340
|
+
const id = r && r.k === 'message' ? String(r.message_id) : null;
|
|
341
|
+
if (id !== null && byMsg.has(id)) { out.push(...byMsg.get(id)); byMsg.delete(id); }
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = {
|
|
347
|
+
placeChildren,
|
|
348
|
+
writeLog, logDir, logRoots, logCapture, logDeriveAdapter, houseTokens, fileNameFor, LOG_VERSION_FLOOR, parseFileName, sessionIdOf, LOG_DERIVE_VERSION,
|
|
349
|
+
};
|
package/editors/sqlite.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// This was `better-sqlite3`, a native module whose prebuilt binary arrives through an
|
|
5
5
|
// npm install script — and npm >= 12 blocks install scripts by default, so a plain
|
|
6
6
|
// `npm i -g memhouse` left no binding at all. Those five adapters then read ZERO
|
|
7
|
-
// sessions and looked exactly like editors the
|
|
7
|
+
// sessions and looked exactly like editors the user does not have. The fix was a flag
|
|
8
8
|
// (`--allow-scripts=better-sqlite3`) that had to be typed on every install and every
|
|
9
9
|
// upgrade, was never remembered by npm, and produced no error when omitted.
|
|
10
10
|
//
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
// need no SQLite at all down with the five that do, and `discover` would report zero
|
|
19
19
|
// editors. Lazy, that throw lands inside each adapter's own try/catch and is reported
|
|
20
20
|
// through adapter-errors like any other unreadable store.
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { pathToFileURL } = require('url');
|
|
24
|
+
|
|
21
25
|
let DatabaseSync = null;
|
|
22
26
|
|
|
23
27
|
/**
|
|
@@ -33,9 +37,32 @@ let DatabaseSync = null;
|
|
|
33
37
|
*/
|
|
34
38
|
function openReadOnly(file) {
|
|
35
39
|
if (!DatabaseSync) ({ DatabaseSync } = require('node:sqlite'));
|
|
40
|
+
// A WAL-mode store opened read-only CREATES <db>-wal and <db>-shm when they are absent, and
|
|
41
|
+
// they stay after close (measured, node:sqlite in Node 24.17): a reader writing into the
|
|
42
|
+
// editor's own folder, on every pass. With a -wal present (the editor is writing, or left one)
|
|
43
|
+
// a plain read-only open is the only correct read: the WAL holds pages the main file lacks.
|
|
44
|
+
// Without one, the main file is the whole database, and immutable=1 reads it creating nothing.
|
|
45
|
+
//
|
|
46
|
+
// immutable=1 also turns locking off, so it is only for a WAL database: a rollback-journal one
|
|
47
|
+
// (Zed's threads.db) is rewritten in place under a lock, and an immutable read during a save is
|
|
48
|
+
// torn — measured: 3,525 "malformed" and 304 corrupt reads in 10,625 against a live writer, none
|
|
49
|
+
// with the plain open, which in rollback mode creates no file. The header says which (byte 18:
|
|
50
|
+
// 1 rollback, 2 WAL).
|
|
51
|
+
if (!fs.existsSync(file)) throw new Error(`unable to open database file: ${file}`);
|
|
52
|
+
if (!fs.existsSync(`${file}-wal`) && isWalHeader(file)) return new DatabaseSync(`${pathToFileURL(path.resolve(file)).href}?immutable=1`, { readOnly: true });
|
|
36
53
|
return new DatabaseSync(file, { readOnly: true });
|
|
37
54
|
}
|
|
38
55
|
|
|
56
|
+
/** True when the file's SQLite header says WAL mode (file format write version, byte 18, is 2). */
|
|
57
|
+
function isWalHeader(file) {
|
|
58
|
+
let fd;
|
|
59
|
+
try {
|
|
60
|
+
fd = fs.openSync(file, 'r');
|
|
61
|
+
const b = Buffer.alloc(20);
|
|
62
|
+
return fs.readSync(fd, b, 0, 20, 0) === 20 && b.toString('latin1', 0, 15) === 'SQLite format 3' && b[18] === 2;
|
|
63
|
+
} catch { return false; } finally { if (fd !== undefined) fs.closeSync(fd); }
|
|
64
|
+
}
|
|
65
|
+
|
|
39
66
|
/**
|
|
40
67
|
* A column's value as text, whether SQLite handed back TEXT or BLOB.
|
|
41
68
|
*
|
package/editors/vscode.js
CHANGED
|
@@ -317,77 +317,6 @@ function getMessages(chat) {
|
|
|
317
317
|
return messages;
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
-
// ============================================================
|
|
321
|
-
// Usage / quota data from GitHub Copilot internal API
|
|
322
|
-
// ============================================================
|
|
323
|
-
|
|
324
|
-
function getCopilotToken() {
|
|
325
|
-
const appsPath = path.join(os.homedir(), '.config', 'github-copilot', 'apps.json');
|
|
326
|
-
try {
|
|
327
|
-
if (!fs.existsSync(appsPath)) return null;
|
|
328
|
-
const data = JSON.parse(fs.readFileSync(appsPath, 'utf-8'));
|
|
329
|
-
// Pick the first available oauth_token
|
|
330
|
-
for (const entry of Object.values(data)) {
|
|
331
|
-
if (entry.oauth_token) return { token: entry.oauth_token, user: entry.user || null };
|
|
332
|
-
}
|
|
333
|
-
} catch {}
|
|
334
|
-
return null;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function fetchCopilotStatus(token) {
|
|
338
|
-
return new Promise((resolve) => {
|
|
339
|
-
const https = require('https');
|
|
340
|
-
const req = https.get('https://api.github.com/copilot_internal/v2/token', {
|
|
341
|
-
headers: {
|
|
342
|
-
'Authorization': `token ${token}`,
|
|
343
|
-
'Accept': 'application/json',
|
|
344
|
-
'User-Agent': 'agentlytics/1.0',
|
|
345
|
-
},
|
|
346
|
-
timeout: 10000,
|
|
347
|
-
}, (res) => {
|
|
348
|
-
let data = '';
|
|
349
|
-
res.on('data', (chunk) => { data += chunk; });
|
|
350
|
-
res.on('end', () => {
|
|
351
|
-
try { resolve(JSON.parse(data)); } catch { resolve(null); }
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
req.on('error', () => resolve(null));
|
|
355
|
-
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
async function getUsage() {
|
|
360
|
-
const { isSubscriptionAccessAllowed } = require('./base');
|
|
361
|
-
if (!isSubscriptionAccessAllowed()) return null;
|
|
362
|
-
const creds = getCopilotToken();
|
|
363
|
-
if (!creds) return null;
|
|
364
|
-
|
|
365
|
-
const status = await fetchCopilotStatus(creds.token);
|
|
366
|
-
if (!status || status.message) return null;
|
|
367
|
-
|
|
368
|
-
return {
|
|
369
|
-
source: 'vscode',
|
|
370
|
-
plan: {
|
|
371
|
-
name: status.sku || null,
|
|
372
|
-
individual: status.individual || false,
|
|
373
|
-
},
|
|
374
|
-
features: {
|
|
375
|
-
chat: status.chat_enabled || false,
|
|
376
|
-
codeReview: status.code_review_enabled || false,
|
|
377
|
-
agentMode: status.agent_mode_auto_approval || false,
|
|
378
|
-
xcode: status.xcode || false,
|
|
379
|
-
mcp: status.mcp || false,
|
|
380
|
-
},
|
|
381
|
-
limits: {
|
|
382
|
-
quotas: status.limited_user_quotas || null,
|
|
383
|
-
resetDate: status.limited_user_reset_date || null,
|
|
384
|
-
},
|
|
385
|
-
user: {
|
|
386
|
-
login: creds.user || null,
|
|
387
|
-
},
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
|
|
391
320
|
const labels = { 'vscode': 'VS Code', 'vscode-insiders': 'VS Code Insiders' };
|
|
392
321
|
|
|
393
322
|
function getArtifacts(folder) {
|
|
@@ -412,4 +341,4 @@ function getMCPServers() {
|
|
|
412
341
|
return results;
|
|
413
342
|
}
|
|
414
343
|
|
|
415
|
-
module.exports = { name, labels, getChats, getMessages,
|
|
344
|
+
module.exports = { name, labels, getChats, getMessages, getArtifacts, getMCPServers };
|
package/editors/windsurf.js
CHANGED
|
@@ -461,148 +461,6 @@ function getMessages(chat) {
|
|
|
461
461
|
return messages;
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
-
// ============================================================
|
|
465
|
-
// Usage / quota data from language server RPC
|
|
466
|
-
// ============================================================
|
|
467
|
-
|
|
468
|
-
function getDevinApiKey(appNames) {
|
|
469
|
-
if (!appNames) return null;
|
|
470
|
-
const names = Array.isArray(appNames) ? appNames : [appNames];
|
|
471
|
-
const keys = ['windsurfAuthStatus', 'devinAuthStatus'];
|
|
472
|
-
try {
|
|
473
|
-
const HOME = os.homedir();
|
|
474
|
-
for (const appName of names) {
|
|
475
|
-
let dbPath;
|
|
476
|
-
switch (process.platform) {
|
|
477
|
-
case 'darwin':
|
|
478
|
-
dbPath = path.join(HOME, 'Library', 'Application Support', appName, 'User', 'globalStorage', 'state.vscdb');
|
|
479
|
-
break;
|
|
480
|
-
case 'win32':
|
|
481
|
-
dbPath = path.join(HOME, 'AppData', 'Roaming', appName, 'User', 'globalStorage', 'state.vscdb');
|
|
482
|
-
break;
|
|
483
|
-
default:
|
|
484
|
-
dbPath = path.join(HOME, '.config', appName, 'User', 'globalStorage', 'state.vscdb');
|
|
485
|
-
}
|
|
486
|
-
if (!fs.existsSync(dbPath)) continue;
|
|
487
|
-
const { openReadOnly, textOf } = require('./sqlite');
|
|
488
|
-
const db = openReadOnly(dbPath);
|
|
489
|
-
try {
|
|
490
|
-
for (const key of keys) {
|
|
491
|
-
const row = db.prepare('SELECT value FROM ItemTable WHERE key = ?').get(key);
|
|
492
|
-
if (!row) continue;
|
|
493
|
-
const parsed = JSON.parse(textOf(row.value));
|
|
494
|
-
if (parsed.apiKey) return parsed.apiKey;
|
|
495
|
-
}
|
|
496
|
-
} finally {
|
|
497
|
-
db.close();
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
return null;
|
|
501
|
-
} catch { return null; }
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function getUsage() {
|
|
505
|
-
const { isSubscriptionAccessAllowed } = require('./base');
|
|
506
|
-
if (!isSubscriptionAccessAllowed()) return [];
|
|
507
|
-
|
|
508
|
-
const results = [];
|
|
509
|
-
|
|
510
|
-
for (const variant of VARIANTS) {
|
|
511
|
-
const ls = getLsForVariant(variant);
|
|
512
|
-
if (!ls) continue;
|
|
513
|
-
|
|
514
|
-
const apiKey = getDevinApiKey(variant.appNames);
|
|
515
|
-
if (!apiKey) continue;
|
|
516
|
-
const body = {
|
|
517
|
-
metadata: {
|
|
518
|
-
api_key: apiKey,
|
|
519
|
-
ide_name: variant.id,
|
|
520
|
-
ide_version: '1.0.0',
|
|
521
|
-
extension_version: '1.0.0',
|
|
522
|
-
locale: 'en',
|
|
523
|
-
},
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
const resp = callRpc(ls.port, ls.csrf, 'GetUserStatus', body, ls.extCsrf);
|
|
527
|
-
if (!resp || !resp.userStatus) continue;
|
|
528
|
-
|
|
529
|
-
const us = resp.userStatus;
|
|
530
|
-
const ps = us.planStatus || {};
|
|
531
|
-
const pi = ps.planInfo || {};
|
|
532
|
-
const modelConfigs = (us.cascadeModelConfigData || {}).clientModelConfigs || [];
|
|
533
|
-
|
|
534
|
-
const models = modelConfigs.map((m) => {
|
|
535
|
-
const qi = m.quotaInfo || {};
|
|
536
|
-
return {
|
|
537
|
-
label: m.label || null,
|
|
538
|
-
model: m.modelOrAlias?.model || null,
|
|
539
|
-
remainingFraction: qi.remainingFraction != null ? qi.remainingFraction : null,
|
|
540
|
-
resetTime: qi.resetTime || null,
|
|
541
|
-
supportsImages: m.supportsImages || false,
|
|
542
|
-
};
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
// Raw values are in internal units (÷100 for display credits)
|
|
546
|
-
const promptAlloc = (ps.availablePromptCredits || 0) / 100;
|
|
547
|
-
const promptUsed = (ps.usedPromptCredits || 0) / 100;
|
|
548
|
-
const flexAlloc = (ps.availableFlexCredits || 0) / 100;
|
|
549
|
-
const flexUsed = (ps.usedFlexCredits || 0) / 100;
|
|
550
|
-
const flowAlloc = (ps.availableFlowCredits || 0) / 100;
|
|
551
|
-
const monthlyDisplay = (pi.monthlyPromptCredits || 0) / 100;
|
|
552
|
-
|
|
553
|
-
const remainingPrompt = Math.max(0, promptAlloc - promptUsed);
|
|
554
|
-
const remainingFlex = Math.max(0, flexAlloc - flexUsed);
|
|
555
|
-
const totalRemaining = remainingPrompt + remainingFlex;
|
|
556
|
-
|
|
557
|
-
// Credit multipliers per model
|
|
558
|
-
const creditMultipliers = (pi.creditMultiplierOverrides || []).reduce((acc, entry) => {
|
|
559
|
-
const model = entry.modelOrAlias?.model;
|
|
560
|
-
if (model && entry.creditMultiplier != null) acc[model] = entry.creditMultiplier;
|
|
561
|
-
return acc;
|
|
562
|
-
}, {});
|
|
563
|
-
|
|
564
|
-
results.push({
|
|
565
|
-
source: variant.id,
|
|
566
|
-
plan: {
|
|
567
|
-
name: pi.planName || null,
|
|
568
|
-
tier: pi.teamsTier || null,
|
|
569
|
-
monthlyPromptCredits: monthlyDisplay,
|
|
570
|
-
monthlyFlowCredits: (pi.monthlyFlowCredits || 0) / 100,
|
|
571
|
-
canBuyMoreCredits: pi.canBuyMoreCredits || false,
|
|
572
|
-
},
|
|
573
|
-
usage: {
|
|
574
|
-
promptCredits: { allocated: promptAlloc, used: promptUsed, remaining: remainingPrompt },
|
|
575
|
-
flexCredits: { allocated: flexAlloc, used: flexUsed, remaining: remainingFlex },
|
|
576
|
-
flowCredits: { allocated: flowAlloc },
|
|
577
|
-
totalRemainingCredits: totalRemaining,
|
|
578
|
-
},
|
|
579
|
-
billingCycle: {
|
|
580
|
-
start: ps.planStart || null,
|
|
581
|
-
end: ps.planEnd || null,
|
|
582
|
-
},
|
|
583
|
-
topUp: ps.topUpStatus ? {
|
|
584
|
-
monthlyAmount: ps.topUpStatus.monthlyTopUpAmount || null,
|
|
585
|
-
increment: ps.topUpStatus.topUpIncrement || null,
|
|
586
|
-
} : null,
|
|
587
|
-
features: {
|
|
588
|
-
webSearch: pi.cascadeWebSearchEnabled || false,
|
|
589
|
-
browser: pi.browserEnabled || false,
|
|
590
|
-
knowledgeBase: pi.knowledgeBaseEnabled || false,
|
|
591
|
-
autoRunCommands: pi.cascadeCanAutoRunCommands || false,
|
|
592
|
-
commitMessages: pi.canGenerateCommitMessages || false,
|
|
593
|
-
},
|
|
594
|
-
models,
|
|
595
|
-
creditMultipliers,
|
|
596
|
-
user: {
|
|
597
|
-
name: us.name || null,
|
|
598
|
-
email: us.email || null,
|
|
599
|
-
},
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
return results.length > 0 ? results : null;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
464
|
function resetCache() { _lsCache = null; }
|
|
607
465
|
|
|
608
466
|
const labels = { 'devin': 'Devin', 'devin-next': 'Devin Next' };
|
|
@@ -631,4 +489,4 @@ function getMCPServers() {
|
|
|
631
489
|
return results;
|
|
632
490
|
}
|
|
633
491
|
|
|
634
|
-
module.exports = { name, sources, legacySources, labels, getChats, getMessages, resetCache,
|
|
492
|
+
module.exports = { name, sources, legacySources, labels, getChats, getMessages, resetCache, getArtifacts, getMCPServers };
|