@north-light/api 0.3.561 → 0.3.563
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/api/client.d.ts +27 -4
- package/dist/api/client.js +53 -0
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/bash-jobs.d.ts +5 -1
- package/dist/api/dto/broker-ops.d.ts +25 -1
- package/dist/api/dto/charges.d.ts +9 -2
- package/dist/api/dto/delivery.d.ts +26 -2
- package/dist/api/dto/execution-start.d.ts +57 -0
- package/dist/api/dto/execution-start.js +2 -0
- package/dist/api/dto/modelauth.d.ts +5 -0
- package/dist/api/dto/node-records.d.ts +16 -0
- package/dist/api/dto/objects.d.ts +1 -1
- package/dist/api/dto/reports.d.ts +0 -3
- package/dist/api/dto/run-events.d.ts +7 -1
- package/dist/api/dto/settings.d.ts +55 -0
- package/dist/api/dto/settings.js +4 -0
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.js +2 -0
- package/dist/api/routes.d.ts +11 -0
- package/dist/api/routes.js +12 -0
- package/dist/core/canvas/paths.d.ts +0 -1
- package/dist/core/canvas/paths.js +0 -3
- package/dist/core/canvas/person-state.d.ts +26 -0
- package/dist/core/canvas/person-state.js +41 -0
- package/dist/core/command-plugins/bundle.d.ts +1 -1
- package/dist/core/command-plugins/bundle.js +5 -5
- package/dist/core/config.d.ts +16 -15
- package/dist/core/config.js +99 -76
- package/dist/core/conversation-store/client.d.ts +2 -0
- package/dist/core/conversation-store/client.js +111 -0
- package/dist/core/conversation-store/index.d.ts +96 -0
- package/dist/core/conversation-store/index.js +62 -0
- package/dist/core/conversation-store/patch.d.ts +3 -0
- package/dist/core/conversation-store/patch.js +192 -0
- package/dist/core/conversation-store/repair.d.ts +45 -0
- package/dist/core/conversation-store/repair.js +84 -0
- package/dist/core/env-name.d.ts +1 -0
- package/dist/core/env-name.js +16 -0
- package/dist/core/events/emit.js +7 -5
- package/dist/core/git.d.ts +28 -0
- package/dist/core/git.js +103 -0
- package/dist/core/installed-plugins.js +12 -7
- package/dist/core/managed-providers.d.ts +72 -0
- package/dist/core/managed-providers.js +61 -0
- package/dist/core/plugin-env.js +1 -2
- package/dist/core/profiles/manifest.d.ts +14 -18
- package/dist/core/profiles/manifest.js +79 -98
- package/dist/core/runtime/broker/attachments-fetch.d.ts +5 -0
- package/dist/core/runtime/broker/attachments-fetch.js +20 -0
- package/dist/core/runtime/broker/daemon-ops.d.ts +10 -1
- package/dist/core/runtime/broker/daemon-ops.js +41 -3
- package/dist/core/runtime/broker/node-token.d.ts +3 -0
- package/dist/core/runtime/broker/node-token.js +16 -0
- package/dist/core/runtime/broker/signal-stream.js +2 -2
- package/dist/core/scoped-state/materialized-plugins.d.ts +18 -0
- package/dist/core/scoped-state/materialized-plugins.js +103 -0
- package/dist/core/scoped-state/person-store.d.ts +72 -0
- package/dist/core/scoped-state/person-store.js +112 -0
- package/dist/core/user-settings.js +1 -1
- package/dist/daemon/broker-signals/bus.d.ts +48 -11
- package/dist/daemon/broker-signals/bus.js +116 -46
- package/dist/shared/inbox-entry-body.js +2 -2
- package/dist/types.d.ts +12 -0
- package/package.json +1 -1
- package/dist/core/canvas/legacy-person.d.ts +0 -4
- package/dist/core/canvas/legacy-person.js +0 -4
- package/dist/core/locked-file.d.ts +0 -27
- package/dist/core/locked-file.js +0 -121
- package/dist/core/profiles/env-store.d.ts +0 -22
- package/dist/core/profiles/env-store.js +0 -166
- package/dist/core/scoped-state/db.d.ts +0 -17
- package/dist/core/scoped-state/db.js +0 -261
- package/dist/core/scoped-state/migrate.d.ts +0 -8
- package/dist/core/scoped-state/migrate.js +0 -187
- package/dist/core/scoped-state/profiles.d.ts +0 -27
- package/dist/core/scoped-state/profiles.js +0 -94
- package/dist/core/scoped-state/providers.d.ts +0 -24
- package/dist/core/scoped-state/providers.js +0 -19
- package/dist/core/scoped-state/schema.d.ts +0 -10
- package/dist/core/scoped-state/schema.js +0 -47
- package/dist/core/scoped-state/settings.d.ts +0 -28
- package/dist/core/scoped-state/settings.js +0 -104
- package/dist/core/sqlite-statements.d.ts +0 -4
- package/dist/core/sqlite-statements.js +0 -17
- package/dist/core/subscription-state.d.ts +0 -129
- package/dist/core/subscription-state.js +0 -299
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { prepared as cachedStatement } from '../sqlite-statements.js';
|
|
2
|
-
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
4
|
-
import { dirname, join, relative } from 'node:path';
|
|
5
|
-
import lockfile from 'proper-lockfile';
|
|
6
|
-
import { CONFIG_FILE, PROFILE_DIR, STATE_FILE } from '../../types.js';
|
|
7
|
-
import { LockedJsonFile } from '../locked-file.js';
|
|
8
|
-
import { assertScopedStoreReady, createScopedStore, discardIncompleteScopedStore, legacyScopedStorePaths, PROFILE_ID_SHAPE, scopedStoreExists, withProjectStateLock, writeOwnerRoot } from './db.js';
|
|
9
|
-
import { canonicalRoot, providerStore, projectStore, userStore } from './paths.js';
|
|
10
|
-
import { profileSpace } from '../layout.js';
|
|
11
|
-
import { createScopedSchema } from './schema.js';
|
|
12
|
-
function json(path) { return JSON.parse(readFileSync(path, 'utf8')); }
|
|
13
|
-
function existsLegacy(paths) { return paths.some(existsSync); }
|
|
14
|
-
function preserve(root, paths) { const target = join(root, '.legacy-scoped-state'); try {
|
|
15
|
-
mkdirSync(target, { mode: 0o700 });
|
|
16
|
-
}
|
|
17
|
-
catch (error) {
|
|
18
|
-
if (error.code !== 'EEXIST' || !statSync(target).isDirectory())
|
|
19
|
-
throw error;
|
|
20
|
-
} chmodSync(target, 0o700); for (const path of paths)
|
|
21
|
-
if (existsSync(path) && statSync(path).isFile()) {
|
|
22
|
-
const fromRoot = relative(root, path);
|
|
23
|
-
if (fromRoot === '' || fromRoot.startsWith('..'))
|
|
24
|
-
throw new Error(`legacy state is outside its owning root: ${path}`);
|
|
25
|
-
const destination = join(target, fromRoot);
|
|
26
|
-
if (dirname(destination) !== target)
|
|
27
|
-
mkdirSync(dirname(destination), { recursive: true, mode: 0o700 });
|
|
28
|
-
copyFileSync(path, destination);
|
|
29
|
-
chmodSync(destination, 0o600);
|
|
30
|
-
} }
|
|
31
|
-
export { initializeFreshScopedStore } from './db.js';
|
|
32
|
-
/** Conversion is intentionally explicit. Deployment coordination must prove all legacy writers at these shared roots are stopped before calling it. */
|
|
33
|
-
export function convertLegacyScopedStore(input) {
|
|
34
|
-
const store = input.kind === 'user' ? userStore(input.root) : input.kind === 'project' ? projectStore(input.root) : providerStore(input.root);
|
|
35
|
-
if (input.kind !== 'project' && scopedStoreExists(store))
|
|
36
|
-
return;
|
|
37
|
-
if (input.kind === 'provider')
|
|
38
|
-
throw new Error('provider conversion requires convertLegacyProviderStore() under coordinated shared-agent-dir activation');
|
|
39
|
-
const paths = legacyScopedStorePaths(store);
|
|
40
|
-
const convert = () => {
|
|
41
|
-
if (input.kind === 'project')
|
|
42
|
-
discardIncompleteScopedStore(store);
|
|
43
|
-
if (scopedStoreExists(store))
|
|
44
|
-
return;
|
|
45
|
-
if (input.kind === 'project' && existsLegacy(paths))
|
|
46
|
-
preserve(store.root, paths);
|
|
47
|
-
createScopedStore(store, (db) => {
|
|
48
|
-
createScopedSchema(db, input.kind);
|
|
49
|
-
const owner = input.kind === 'user' ? 'user' : 'project';
|
|
50
|
-
writeOwnerRoot(db, owner, store.root);
|
|
51
|
-
if (input.kind === 'user')
|
|
52
|
-
importUser(db, store.root);
|
|
53
|
-
else
|
|
54
|
-
importState(db, owner, join(store.root, STATE_FILE));
|
|
55
|
-
});
|
|
56
|
-
if (input.kind === 'user' && existsLegacy(paths))
|
|
57
|
-
preserve(store.root, paths);
|
|
58
|
-
};
|
|
59
|
-
if (input.kind === 'project')
|
|
60
|
-
withProjectStateLock(store.root, () => { convert(); assertScopedStoreReady(store); });
|
|
61
|
-
else
|
|
62
|
-
convert();
|
|
63
|
-
}
|
|
64
|
-
function parseLegacyProviderPool(path) {
|
|
65
|
-
if (!existsSync(path))
|
|
66
|
-
return [];
|
|
67
|
-
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
68
|
-
if (!Array.isArray(raw))
|
|
69
|
-
throw new Error(`legacy subscription pool is malformed: ${path}`);
|
|
70
|
-
const labels = new Set();
|
|
71
|
-
return raw.map((item, position) => {
|
|
72
|
-
if (item === null || typeof item !== 'object' || Array.isArray(item))
|
|
73
|
-
throw new Error(`legacy subscription pool entry ${position + 1} is malformed: ${path}`);
|
|
74
|
-
const value = item;
|
|
75
|
-
const label = typeof value.label === 'string' ? value.label.trim() : '';
|
|
76
|
-
const refresh = typeof value.refresh === 'string' ? value.refresh.trim() : '';
|
|
77
|
-
const access = typeof value.access === 'string' ? value.access.trim() : '';
|
|
78
|
-
const expires = Number(value.expires);
|
|
79
|
-
if (!label || !refresh || !access || !Number.isFinite(expires))
|
|
80
|
-
throw new Error(`legacy subscription pool entry ${position + 1} is incomplete: ${path}`);
|
|
81
|
-
const labelKey = label.toLowerCase();
|
|
82
|
-
if (labels.has(labelKey))
|
|
83
|
-
throw new Error(`legacy subscription pool has duplicate label "${label}": ${path}`);
|
|
84
|
-
labels.add(labelKey);
|
|
85
|
-
const timing = (field) => Number.isFinite(Number(value[field])) ? Number(value[field]) : 0;
|
|
86
|
-
const accountId = typeof value.accountId === 'string' && value.accountId.trim() !== '' ? value.accountId.trim() : undefined;
|
|
87
|
-
return { label, refresh, access, expires, rateLimitedUntil: timing('rateLimitedUntil'), lastAttemptAt: timing('lastAttemptAt'), lastRateLimitedAt: timing('lastRateLimitedAt'), ...(value.authFailure === 'invalid_grant' ? { authFailure: 'invalid_grant' } : {}), ...(accountId ? { accountId } : {}) };
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
function parseLegacyRotation(path) {
|
|
91
|
-
if (!existsSync(path))
|
|
92
|
-
return {};
|
|
93
|
-
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
94
|
-
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
95
|
-
throw new Error(`legacy provider rotation settings are malformed: ${path}`);
|
|
96
|
-
const value = raw;
|
|
97
|
-
return { ...(value.defaultFallbackStrength === 'ultra' || value.defaultFallbackStrength === 'strong' || value.defaultFallbackStrength === 'medium' || value.defaultFallbackStrength === 'light' ? { defaultFallbackStrength: value.defaultFallbackStrength } : {}), ...(typeof value.revertWhenAvailable === 'boolean' ? { revertWhenAvailable: value.revertWhenAvailable } : {}) };
|
|
98
|
-
}
|
|
99
|
-
async function withLegacyProviderLocks(paths, run) {
|
|
100
|
-
const releases = [];
|
|
101
|
-
try {
|
|
102
|
-
for (const path of paths)
|
|
103
|
-
if (existsSync(path))
|
|
104
|
-
releases.push(await lockfile.lock(path, { realpath: false, retries: 0, stale: 30_000, update: 10_000 }));
|
|
105
|
-
return await run();
|
|
106
|
-
}
|
|
107
|
-
finally {
|
|
108
|
-
await Promise.allSettled(releases.reverse().map((release) => release()));
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
/** Converts legacy provider pools only after the activation owner has stopped every writer sharing this agent directory. */
|
|
112
|
-
export async function convertLegacyProviderStore(agentDir) {
|
|
113
|
-
const store = providerStore(agentDir);
|
|
114
|
-
if (scopedStoreExists(store))
|
|
115
|
-
return;
|
|
116
|
-
const paths = legacyScopedStorePaths(store);
|
|
117
|
-
await withLegacyProviderLocks(paths, async () => {
|
|
118
|
-
if (scopedStoreExists(store))
|
|
119
|
-
return;
|
|
120
|
-
const pools = new Map(paths.slice(0, 2).map((path, index) => [index === 0 ? 'anthropic' : 'openai-codex', parseLegacyProviderPool(path)]));
|
|
121
|
-
const rotation = parseLegacyRotation(paths[2]);
|
|
122
|
-
mkdirSync(store.root, { recursive: true, mode: 0o700 });
|
|
123
|
-
preserve(store.root, paths);
|
|
124
|
-
for (const [provider, pool] of pools) {
|
|
125
|
-
const accounts = Object.fromEntries(pool.map((entry) => { const id = randomUUID(); return [id, { refresh: entry.refresh, access: entry.access, expires: entry.expires, ...(entry.accountId ? { accountId: entry.accountId } : {}) }]; }));
|
|
126
|
-
const file = new LockedJsonFile(join(store.root, `${provider}-credentials.json`), true, '{"version":1,"accounts":{}}\n');
|
|
127
|
-
await file.withLockAsync(async (_current, _held, publish) => { publish(`${JSON.stringify({ version: 1, accounts })}\n`); return { result: undefined }; });
|
|
128
|
-
}
|
|
129
|
-
createScopedStore(store, (db) => {
|
|
130
|
-
createScopedSchema(db, 'provider');
|
|
131
|
-
for (const [provider, pool] of pools) {
|
|
132
|
-
const accounts = JSON.parse(readFileSync(join(store.root, `${provider}-credentials.json`), 'utf8'));
|
|
133
|
-
const ids = Object.keys(accounts.accounts);
|
|
134
|
-
const insert = cachedStatement(db, 'INSERT INTO managed_accounts(provider_id,id,label,label_key,position,last_attempt_at,last_rate_limited_at,rate_limited_until,auth_failure) VALUES(?,?,?,?,?,?,?,?,?)');
|
|
135
|
-
pool.forEach((entry, position) => insert.run(provider, ids[position], entry.label, entry.label.toLowerCase(), position, entry.lastAttemptAt, entry.lastRateLimitedAt, entry.rateLimitedUntil, entry.authFailure ?? null));
|
|
136
|
-
}
|
|
137
|
-
if (rotation.defaultFallbackStrength !== undefined || rotation.revertWhenAvailable !== undefined)
|
|
138
|
-
cachedStatement(db, 'INSERT INTO rotation_settings(singleton,default_fallback_strength,revert_when_available) VALUES(1,?,?)').run(rotation.defaultFallbackStrength ?? null, rotation.revertWhenAvailable === undefined ? null : rotation.revertWhenAvailable ? 1 : 0);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
function importSettings(db, owner, path) { if (!existsSync(path))
|
|
143
|
-
return; const raw = json(path); if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
144
|
-
throw new Error(`legacy config is malformed: ${path}`); cachedStatement(db, 'INSERT INTO scope_settings(owner_key,raw_json) VALUES(?,?)').run(owner, JSON.stringify(raw)); }
|
|
145
|
-
function importState(db, owner, path) { if (!existsSync(path))
|
|
146
|
-
return; const raw = json(path); if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
147
|
-
throw new Error(`legacy state is malformed: ${path}`); const state = raw; cachedStatement(db, 'INSERT INTO scope_bookkeeping(owner_key,last_self_check,active_canvas) VALUES(?,?,?)').run(owner, state.last_self_check ?? null, state.activeCanvas ?? null); const market = cachedStatement(db, 'INSERT INTO scope_marketplaces(owner_key,name,last_updated) VALUES(?,?,?)'); for (const [name, value] of Object.entries(state.marketplaces ?? {}))
|
|
148
|
-
market.run(owner, name, value.last_updated ?? null); const plugins = cachedStatement(db, 'INSERT INTO scope_plugins(owner_key,name,last_updated,etag,checked_at) VALUES(?,?,?,?,?)'); for (const [name, value] of Object.entries(state.plugins ?? {}))
|
|
149
|
-
plugins.run(owner, name, value.last_updated ?? null, value.etag ?? null, value.checked_at ?? null); }
|
|
150
|
-
function importUser(db, root) {
|
|
151
|
-
importSettings(db, 'user', join(root, CONFIG_FILE));
|
|
152
|
-
importState(db, 'user', join(root, STATE_FILE));
|
|
153
|
-
const profiles = join(root, PROFILE_DIR);
|
|
154
|
-
if (existsSync(profiles))
|
|
155
|
-
for (const entry of readdirSync(profiles, { withFileTypes: true })) {
|
|
156
|
-
if (!entry.isDirectory() || !PROFILE_ID_SHAPE.test(entry.name))
|
|
157
|
-
continue;
|
|
158
|
-
const id = entry.name;
|
|
159
|
-
const profileRoot = join(profiles, id);
|
|
160
|
-
const path = join(profileRoot, 'profile.json');
|
|
161
|
-
if (!existsSync(path))
|
|
162
|
-
throw new Error(`legacy profile manifest is missing: ${path}`);
|
|
163
|
-
const manifest = json(path);
|
|
164
|
-
if (!manifest || typeof manifest !== 'object' || !Array.isArray(manifest.projects) || typeof manifest.name !== 'string' || typeof manifest.created_at !== 'string')
|
|
165
|
-
throw new Error(`legacy profile manifest is malformed: ${path}`);
|
|
166
|
-
cachedStatement(db, 'INSERT INTO profiles(id,name,created_at,last_used_at,paused_at,default_kind,grantee,metadata_json) VALUES(?,?,?,?,?,?,?,?)').run(id, manifest.name, manifest.created_at, manifest.last_used_at ?? null, manifest.paused_at ?? null, manifest.default_kind ?? null, manifest.grantee ?? null, JSON.stringify(manifest.metadata ?? {}));
|
|
167
|
-
cachedStatement(db, 'INSERT INTO scope_owners(owner_key,kind,root,profile_id) VALUES(?,?,?,?)').run(`profile:${id}`, 'profile', canonicalRoot(profileSpace(manifest.grantee ?? 'app:terminal', id).dir), id);
|
|
168
|
-
const project = cachedStatement(db, 'INSERT INTO profile_projects(profile_id,path,position) VALUES(?,?,?)');
|
|
169
|
-
const cap = cachedStatement(db, 'INSERT INTO legacy_project_caps(profile_id,path,memory) VALUES(?,?,?)');
|
|
170
|
-
manifest.projects.forEach((p, position) => { project.run(id, p.path, position); if (p.memory !== undefined && p.memory !== 'content')
|
|
171
|
-
cap.run(id, p.path, p.memory); });
|
|
172
|
-
importSettings(db, `profile:${id}`, join(profileRoot, CONFIG_FILE));
|
|
173
|
-
importState(db, `profile:${id}`, join(profileRoot, STATE_FILE));
|
|
174
|
-
}
|
|
175
|
-
const defaults = join(root, 'profile-defaults.json');
|
|
176
|
-
if (existsSync(defaults)) {
|
|
177
|
-
const raw = json(defaults);
|
|
178
|
-
if (raw === null || typeof raw !== 'object' || Array.isArray(raw))
|
|
179
|
-
throw new Error(`legacy profile defaults are malformed: ${defaults}`);
|
|
180
|
-
const insert = cachedStatement(db, 'INSERT INTO profile_defaults(directory,profile_id) VALUES(?,?)');
|
|
181
|
-
for (const [directory, id] of Object.entries(raw)) {
|
|
182
|
-
if (typeof id !== 'string')
|
|
183
|
-
throw new Error(`legacy profile default is malformed: ${defaults}`);
|
|
184
|
-
insert.run(directory, id);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { ProfileManifest } from '../../types.js';
|
|
2
|
-
export type StoredProfile = {
|
|
3
|
-
profileId: string;
|
|
4
|
-
manifest: ProfileManifest;
|
|
5
|
-
};
|
|
6
|
-
export declare function listStoredProfiles(root: string): StoredProfile[];
|
|
7
|
-
export declare function getStoredProfile(root: string, id: string): StoredProfile | null;
|
|
8
|
-
/** Pause gates need only this column; do not hydrate projects or JSON manifests. */
|
|
9
|
-
export declare function profilePaused(root: string, id: string): boolean;
|
|
10
|
-
export declare function pausedProfileIds(root: string): Set<string>;
|
|
11
|
-
export declare function insertStoredProfile(root: string, profile: StoredProfile): void;
|
|
12
|
-
export declare function updateStoredProfile(root: string, id: string, mutate: (current: ProfileManifest) => ProfileManifest): StoredProfile | null;
|
|
13
|
-
export declare function deleteStoredProfile(root: string, id: string): boolean;
|
|
14
|
-
export declare function rewriteLocalProfileGrantees(root: string): void;
|
|
15
|
-
export declare function readStoredDefaults(root: string): Record<string, string>;
|
|
16
|
-
export declare function mutateStoredDefaults(root: string, mutate: (current: Record<string, string>) => Record<string, string>): Record<string, string>;
|
|
17
|
-
/** Pre-canvas per-project memory caps (`none|name|preview`) the canvas import has not converted yet. */
|
|
18
|
-
export declare function listLegacyProjectCaps(root: string): Array<{
|
|
19
|
-
profileId: string;
|
|
20
|
-
path: string;
|
|
21
|
-
memory: string;
|
|
22
|
-
}>;
|
|
23
|
-
/** Store each converted profile's delivery limits and delete every legacy cap row, in one write. */
|
|
24
|
-
export declare function commitConvertedProjectCaps(root: string, limits: ReadonlyArray<{
|
|
25
|
-
profileId: string;
|
|
26
|
-
deliveryLimits: Record<string, string>;
|
|
27
|
-
}>): void;
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { prepared as cachedStatement } from '../sqlite-statements.js';
|
|
2
|
-
import { canonicalRoot, userStore } from './paths.js';
|
|
3
|
-
import { profileSpace } from '../layout.js';
|
|
4
|
-
import { openScopedStore, withScopedWrite } from './db.js';
|
|
5
|
-
import { LEGACY_LOCAL_PERSON } from '../canvas/legacy-person.js';
|
|
6
|
-
function store(root) { return userStore(root); }
|
|
7
|
-
function rowToProfile(row, projects) {
|
|
8
|
-
return { profileId: row.id, manifest: { schema_version: 1, name: row.name, projects, paused_at: row.paused_at, ...(row.default_kind === null ? {} : { default_kind: row.default_kind }), ...(row.grantee === null ? {} : { grantee: row.grantee }), ...(row.environment_id === null ? {} : { environment_id: row.environment_id }), ...(row.delivery_limits_json === '{}' ? {} : { delivery_limits: JSON.parse(row.delivery_limits_json) }), ...(Object.keys(JSON.parse(row.metadata_json)).length === 0 ? {} : { metadata: JSON.parse(row.metadata_json) }), created_at: row.created_at, last_used_at: row.last_used_at } };
|
|
9
|
-
}
|
|
10
|
-
function get(db, id) {
|
|
11
|
-
const row = cachedStatement(db, 'SELECT id, name, created_at, last_used_at, paused_at, default_kind, grantee, metadata_json, delivery_limits_json, environment_id FROM profiles WHERE id=?').get(id);
|
|
12
|
-
if (!row)
|
|
13
|
-
return null;
|
|
14
|
-
const projects = cachedStatement(db, 'SELECT path FROM profile_projects WHERE profile_id=? ORDER BY position').all(id);
|
|
15
|
-
return rowToProfile(row, projects);
|
|
16
|
-
}
|
|
17
|
-
export function listStoredProfiles(root) {
|
|
18
|
-
const db = openScopedStore(store(root));
|
|
19
|
-
const ids = cachedStatement(db, 'SELECT id FROM profiles ORDER BY rowid').all();
|
|
20
|
-
return ids.map(({ id }) => get(db, id)).filter(Boolean);
|
|
21
|
-
}
|
|
22
|
-
export function getStoredProfile(root, id) { return get(openScopedStore(store(root)), id); }
|
|
23
|
-
/** Pause gates need only this column; do not hydrate projects or JSON manifests. */
|
|
24
|
-
export function profilePaused(root, id) {
|
|
25
|
-
const row = cachedStatement(openScopedStore(store(root)), 'SELECT paused_at FROM profiles WHERE id=?').get(id);
|
|
26
|
-
return row?.paused_at != null;
|
|
27
|
-
}
|
|
28
|
-
export function pausedProfileIds(root) {
|
|
29
|
-
const rows = cachedStatement(openScopedStore(store(root)), 'SELECT id FROM profiles WHERE paused_at IS NOT NULL').all();
|
|
30
|
-
return new Set(rows.map((row) => row.id));
|
|
31
|
-
}
|
|
32
|
-
export function insertStoredProfile(root, profile) {
|
|
33
|
-
withScopedWrite(store(root), (db) => {
|
|
34
|
-
cachedStatement(db, 'INSERT INTO profiles(id,name,created_at,last_used_at,paused_at,default_kind,grantee,metadata_json,delivery_limits_json,environment_id) VALUES(?,?,?,?,?,?,?,?,?,?)').run(profile.profileId, profile.manifest.name, profile.manifest.created_at, profile.manifest.last_used_at, profile.manifest.paused_at, profile.manifest.default_kind ?? null, profile.manifest.grantee ?? null, JSON.stringify(profile.manifest.metadata ?? {}), JSON.stringify(profile.manifest.delivery_limits ?? {}), profile.manifest.environment_id ?? null);
|
|
35
|
-
cachedStatement(db, 'INSERT INTO scope_owners(owner_key,kind,root,profile_id) VALUES(?,?,?,?)').run(`profile:${profile.profileId}`, 'profile', canonicalRoot(profileSpace(profile.manifest.grantee ?? 'app:terminal', profile.profileId).dir), profile.profileId);
|
|
36
|
-
const insert = cachedStatement(db, 'INSERT INTO profile_projects(profile_id,path,position) VALUES(?,?,?)');
|
|
37
|
-
profile.manifest.projects.forEach((project, position) => insert.run(profile.profileId, project.path, position));
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
export function updateStoredProfile(root, id, mutate) {
|
|
41
|
-
return withScopedWrite(store(root), (db) => {
|
|
42
|
-
const current = get(db, id);
|
|
43
|
-
if (!current)
|
|
44
|
-
return null;
|
|
45
|
-
const next = mutate(current.manifest);
|
|
46
|
-
cachedStatement(db, 'UPDATE profiles SET name=?, last_used_at=?, paused_at=?, default_kind=?, grantee=?, metadata_json=?, delivery_limits_json=?, environment_id=? WHERE id=?').run(next.name, next.last_used_at, next.paused_at, next.default_kind ?? null, next.grantee ?? null, JSON.stringify(next.metadata ?? {}), JSON.stringify(next.delivery_limits ?? {}), next.environment_id ?? null, id);
|
|
47
|
-
cachedStatement(db, 'DELETE FROM profile_projects WHERE profile_id=?').run(id);
|
|
48
|
-
const insert = cachedStatement(db, 'INSERT INTO profile_projects(profile_id,path,position) VALUES(?,?,?)');
|
|
49
|
-
next.projects.forEach((project, position) => insert.run(id, project.path, position));
|
|
50
|
-
return { profileId: id, manifest: next };
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
export function deleteStoredProfile(root, id) {
|
|
54
|
-
return withScopedWrite(store(root), (db) => {
|
|
55
|
-
if (!get(db, id))
|
|
56
|
-
return false;
|
|
57
|
-
const owner = `profile:${id}`;
|
|
58
|
-
cachedStatement(db, 'DELETE FROM scope_settings WHERE owner_key=?').run(owner);
|
|
59
|
-
cachedStatement(db, 'DELETE FROM scope_bookkeeping WHERE owner_key=?').run(owner);
|
|
60
|
-
cachedStatement(db, 'DELETE FROM scope_marketplaces WHERE owner_key=?').run(owner);
|
|
61
|
-
cachedStatement(db, 'DELETE FROM scope_plugins WHERE owner_key=?').run(owner);
|
|
62
|
-
cachedStatement(db, 'DELETE FROM scope_owners WHERE owner_key=?').run(owner);
|
|
63
|
-
cachedStatement(db, 'DELETE FROM profiles WHERE id=?').run(id);
|
|
64
|
-
return true;
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
export function rewriteLocalProfileGrantees(root) {
|
|
68
|
-
withScopedWrite(store(root), (db) => {
|
|
69
|
-
cachedStatement(db, "UPDATE profiles SET grantee = 'app:terminal' WHERE grantee = ?").run(LEGACY_LOCAL_PERSON);
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
export function readStoredDefaults(root) {
|
|
73
|
-
const db = openScopedStore(store(root));
|
|
74
|
-
return Object.fromEntries(cachedStatement(db, 'SELECT directory,profile_id FROM profile_defaults').all().map((row) => [row.directory, row.profile_id]));
|
|
75
|
-
}
|
|
76
|
-
export function mutateStoredDefaults(root, mutate) {
|
|
77
|
-
return withScopedWrite(store(root), (db) => { const current = Object.fromEntries(cachedStatement(db, 'SELECT directory,profile_id FROM profile_defaults').all().map((row) => [row.directory, row.profile_id])); const next = mutate(current); cachedStatement(db, 'DELETE FROM profile_defaults').run(); const insert = cachedStatement(db, 'INSERT INTO profile_defaults(directory,profile_id) VALUES(?,?)'); for (const [directory, id] of Object.entries(next))
|
|
78
|
-
insert.run(directory, id); return next; });
|
|
79
|
-
}
|
|
80
|
-
/** Pre-canvas per-project memory caps (`none|name|preview`) the canvas import has not converted yet. */
|
|
81
|
-
export function listLegacyProjectCaps(root) {
|
|
82
|
-
return cachedStatement(openScopedStore(store(root)), 'SELECT profile_id, path, memory FROM legacy_project_caps ORDER BY profile_id, path').all()
|
|
83
|
-
.map((r) => ({ profileId: r.profile_id, path: r.path, memory: r.memory }));
|
|
84
|
-
}
|
|
85
|
-
/** Store each converted profile's delivery limits and delete every legacy cap row, in one write. */
|
|
86
|
-
export function commitConvertedProjectCaps(root, limits) {
|
|
87
|
-
withScopedWrite(store(root), (db) => {
|
|
88
|
-
const set = cachedStatement(db, 'UPDATE profiles SET delivery_limits_json=? WHERE id=?');
|
|
89
|
-
for (const l of limits)
|
|
90
|
-
if (set.run(JSON.stringify(l.deliveryLimits), l.profileId).changes !== 1)
|
|
91
|
-
throw new Error(`no stored profile ${l.profileId}`);
|
|
92
|
-
cachedStatement(db, 'DELETE FROM legacy_project_caps').run();
|
|
93
|
-
});
|
|
94
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
export type ProviderRow = {
|
|
3
|
-
providerId: string;
|
|
4
|
-
id: string;
|
|
5
|
-
label: string;
|
|
6
|
-
labelKey: string;
|
|
7
|
-
position: number;
|
|
8
|
-
lastAttemptAt: number;
|
|
9
|
-
lastRateLimitedAt: number;
|
|
10
|
-
rateLimitedUntil: number;
|
|
11
|
-
authFailure?: 'invalid_grant';
|
|
12
|
-
};
|
|
13
|
-
/** One model family's cooldown on one account (`managed_account_model_cooldowns`). */
|
|
14
|
-
export type ProviderModelCooldown = {
|
|
15
|
-
family: string;
|
|
16
|
-
rateLimitedUntil: number;
|
|
17
|
-
lastRateLimitedAt: number;
|
|
18
|
-
};
|
|
19
|
-
export declare function providerRows(agentDir: string, providerId: string): ProviderRow[];
|
|
20
|
-
export declare function providerRowByReference(db: DatabaseSync, providerId: string, ref: string): ProviderRow | undefined;
|
|
21
|
-
export declare function providerModelCooldowns(db: DatabaseSync, providerId: string): Map<string, ProviderModelCooldown[]>;
|
|
22
|
-
export declare function providerModelCooldownsFor(agentDir: string, providerId: string): Map<string, ProviderModelCooldown[]>;
|
|
23
|
-
export declare function providerWrite<T>(agentDir: string, run: (db: DatabaseSync) => T): T;
|
|
24
|
-
export declare function providerRevision(agentDir: string): number;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { prepared as cachedStatement } from '../sqlite-statements.js';
|
|
2
|
-
import { openScopedStore, withScopedWrite } from './db.js';
|
|
3
|
-
import { providerStore } from './paths.js';
|
|
4
|
-
function map(row) { return { providerId: row.provider_id, id: row.id, label: row.label, labelKey: row.label_key, position: row.position, lastAttemptAt: row.last_attempt_at, lastRateLimitedAt: row.last_rate_limited_at, rateLimitedUntil: row.rate_limited_until, ...(row.auth_failure ? { authFailure: row.auth_failure } : {}) }; }
|
|
5
|
-
const ROW_COLUMNS = 'provider_id,id,label,label_key,position,last_attempt_at,last_rate_limited_at,rate_limited_until,auth_failure';
|
|
6
|
-
export function providerRows(agentDir, providerId) { const db = openScopedStore(providerStore(agentDir)); return db.prepare(`SELECT ${ROW_COLUMNS} FROM managed_accounts WHERE provider_id=? ORDER BY position`).all(providerId).map(map); }
|
|
7
|
-
export function providerRowByReference(db, providerId, ref) { const needle = ref.trim().toLowerCase(); const position = Number.parseInt(needle, 10); const row = (Number.isFinite(position) && String(position) === needle && position >= 1 ? db.prepare(`SELECT ${ROW_COLUMNS} FROM managed_accounts WHERE provider_id=? AND position=?`).get(providerId, position - 1) : db.prepare(`SELECT ${ROW_COLUMNS} FROM managed_accounts WHERE provider_id=? AND label_key=?`).get(providerId, needle)); return row ? map(row) : undefined; }
|
|
8
|
-
export function providerModelCooldowns(db, providerId) {
|
|
9
|
-
const out = new Map();
|
|
10
|
-
for (const row of cachedStatement(db, 'SELECT account_id,family,rate_limited_until,last_rate_limited_at FROM managed_account_model_cooldowns WHERE provider_id=? ORDER BY family').all(providerId)) {
|
|
11
|
-
const list = out.get(row.account_id) ?? [];
|
|
12
|
-
list.push({ family: row.family, rateLimitedUntil: row.rate_limited_until, lastRateLimitedAt: row.last_rate_limited_at });
|
|
13
|
-
out.set(row.account_id, list);
|
|
14
|
-
}
|
|
15
|
-
return out;
|
|
16
|
-
}
|
|
17
|
-
export function providerModelCooldownsFor(agentDir, providerId) { return providerModelCooldowns(openScopedStore(providerStore(agentDir)), providerId); }
|
|
18
|
-
export function providerWrite(agentDir, run) { return withScopedWrite(providerStore(agentDir), (db) => { const value = run(db); cachedStatement(db, 'UPDATE provider_revision SET revision=revision+1 WHERE singleton=1').run(); return value; }); }
|
|
19
|
-
export function providerRevision(agentDir) { const db = openScopedStore(providerStore(agentDir)); return cachedStatement(db, 'SELECT revision FROM provider_revision WHERE singleton=1').get().revision; }
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
-
export declare const SCOPED_SCHEMA_VERSION = 5;
|
|
3
|
-
export declare const PROVIDER_SCHEMA_VERSION = 1;
|
|
4
|
-
/** `profiles.environment_id` (null = the machine environment) is additive to version 5: a store written before it gains the
|
|
5
|
-
* column on open (`upgradeScopedStore`), and an older generation sharing the store names its columns and never reads it, so
|
|
6
|
-
* the version stays 5 and a runtime rollback keeps opening the store. */
|
|
7
|
-
export declare const PROFILE_ENVIRONMENT_COLUMN = "environment_id";
|
|
8
|
-
/** Adds the model-cooldown table to a provider store that predates it; a no-op (no write) once present. */
|
|
9
|
-
export declare function ensureProviderModelCooldowns(db: DatabaseSync): void;
|
|
10
|
-
export declare function createScopedSchema(db: DatabaseSync, kind: 'user' | 'project' | 'provider'): void;
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { prepared as cachedStatement } from '../sqlite-statements.js';
|
|
2
|
-
export const SCOPED_SCHEMA_VERSION = 5;
|
|
3
|
-
export const PROVIDER_SCHEMA_VERSION = 1;
|
|
4
|
-
/** `profiles.environment_id` (null = the machine environment) is additive to version 5: a store written before it gains the
|
|
5
|
-
* column on open (`upgradeScopedStore`), and an older generation sharing the store names its columns and never reads it, so
|
|
6
|
-
* the version stays 5 and a runtime rollback keeps opening the store. */
|
|
7
|
-
export const PROFILE_ENVIRONMENT_COLUMN = 'environment_id';
|
|
8
|
-
const SCOPE_SCHEMA = `
|
|
9
|
-
CREATE TABLE scope_owners (owner_key TEXT PRIMARY KEY, kind TEXT NOT NULL CHECK(kind IN ('user','profile','project')), root TEXT NOT NULL UNIQUE, profile_id TEXT UNIQUE);
|
|
10
|
-
CREATE TABLE profiles (id TEXT PRIMARY KEY, name TEXT NOT NULL, created_at TEXT NOT NULL, last_used_at TEXT, paused_at TEXT, default_kind TEXT, grantee TEXT, metadata_json TEXT NOT NULL CHECK(json_valid(metadata_json)), delivery_limits_json TEXT NOT NULL DEFAULT '{}' CHECK(json_valid(delivery_limits_json)), environment_id TEXT);
|
|
11
|
-
CREATE TABLE profile_projects (profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, path TEXT NOT NULL, position INTEGER NOT NULL, PRIMARY KEY(profile_id,path), UNIQUE(profile_id,position));
|
|
12
|
-
-- Pre-canvas per-project memory caps awaiting the canvas import, which turns them into delivery_limits and deletes the rows.
|
|
13
|
-
CREATE TABLE legacy_project_caps (profile_id TEXT NOT NULL, path TEXT NOT NULL, memory TEXT NOT NULL CHECK(memory IN ('none','name','preview')), PRIMARY KEY(profile_id,path));
|
|
14
|
-
CREATE TABLE profile_defaults (directory TEXT PRIMARY KEY, profile_id TEXT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE);
|
|
15
|
-
CREATE TABLE scope_settings (owner_key TEXT PRIMARY KEY REFERENCES scope_owners(owner_key), raw_json TEXT NOT NULL CHECK(json_valid(raw_json)));
|
|
16
|
-
CREATE TABLE scope_bookkeeping (owner_key TEXT PRIMARY KEY REFERENCES scope_owners(owner_key), last_self_check TEXT, active_canvas TEXT);
|
|
17
|
-
CREATE TABLE scope_marketplaces (owner_key TEXT NOT NULL REFERENCES scope_owners(owner_key), name TEXT NOT NULL, last_updated TEXT, PRIMARY KEY(owner_key,name));
|
|
18
|
-
CREATE TABLE scope_plugins (owner_key TEXT NOT NULL REFERENCES scope_owners(owner_key), name TEXT NOT NULL, last_updated TEXT, etag TEXT, checked_at TEXT, PRIMARY KEY(owner_key,name));`;
|
|
19
|
-
// A subscription 429 on one model family's window (Anthropic `seven_day_opus` /
|
|
20
|
-
// `seven_day_sonnet`) cools only that family on that account. Additive to v1: a
|
|
21
|
-
// store written before this table existed gains it on open (ensureProviderModelCooldowns),
|
|
22
|
-
// and older crtr generations sharing the store simply never read it, so the provider
|
|
23
|
-
// schema version stays 1 and a runtime rollback keeps opening the store.
|
|
24
|
-
const MODEL_COOLDOWNS_TABLE = 'CREATE TABLE IF NOT EXISTS managed_account_model_cooldowns (provider_id TEXT NOT NULL, account_id TEXT NOT NULL, family TEXT NOT NULL, rate_limited_until INTEGER NOT NULL, last_rate_limited_at INTEGER NOT NULL, PRIMARY KEY(provider_id,account_id,family));';
|
|
25
|
-
/** Adds the model-cooldown table to a provider store that predates it; a no-op (no write) once present. */
|
|
26
|
-
export function ensureProviderModelCooldowns(db) {
|
|
27
|
-
const present = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='managed_account_model_cooldowns'").get();
|
|
28
|
-
if (present === undefined)
|
|
29
|
-
db.exec(MODEL_COOLDOWNS_TABLE);
|
|
30
|
-
}
|
|
31
|
-
const PROVIDER_SCHEMA = `
|
|
32
|
-
CREATE TABLE managed_accounts (provider_id TEXT NOT NULL, id TEXT NOT NULL, label TEXT NOT NULL, label_key TEXT NOT NULL, position INTEGER NOT NULL, last_attempt_at INTEGER NOT NULL, last_rate_limited_at INTEGER NOT NULL, rate_limited_until INTEGER NOT NULL, auth_failure TEXT CHECK(auth_failure IS NULL OR auth_failure='invalid_grant'), PRIMARY KEY(provider_id,id), UNIQUE(provider_id,label_key));
|
|
33
|
-
CREATE INDEX managed_accounts_order ON managed_accounts(provider_id,position);
|
|
34
|
-
CREATE TABLE rotation_settings (singleton INTEGER PRIMARY KEY CHECK(singleton=1), default_fallback_strength TEXT, revert_when_available INTEGER CHECK(revert_when_available IN (0,1)));
|
|
35
|
-
CREATE TABLE provider_revision (singleton INTEGER PRIMARY KEY CHECK(singleton=1), revision INTEGER NOT NULL);
|
|
36
|
-
${MODEL_COOLDOWNS_TABLE}`;
|
|
37
|
-
export function createScopedSchema(db, kind) {
|
|
38
|
-
db.exec(kind === 'provider' ? PROVIDER_SCHEMA : SCOPE_SCHEMA);
|
|
39
|
-
if (kind !== 'provider') {
|
|
40
|
-
const ownerKey = kind === 'user' ? 'user' : 'project';
|
|
41
|
-
cachedStatement(db, 'INSERT INTO scope_owners(owner_key, kind, root, profile_id) VALUES (?, ?, ?, NULL)').run(ownerKey, kind, '');
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
cachedStatement(db, 'INSERT INTO provider_revision(singleton, revision) VALUES (1, 0)').run();
|
|
45
|
-
}
|
|
46
|
-
db.exec(`PRAGMA user_version = ${kind === 'provider' ? PROVIDER_SCHEMA_VERSION : SCOPED_SCHEMA_VERSION}`);
|
|
47
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { ScopeState } from '../../types.js';
|
|
2
|
-
import type { ScopedStore } from './paths.js';
|
|
3
|
-
export type SettingsOwner = {
|
|
4
|
-
store: ScopedStore;
|
|
5
|
-
key: string;
|
|
6
|
-
kind: 'user' | 'profile' | 'project';
|
|
7
|
-
root: string;
|
|
8
|
-
profileId?: string;
|
|
9
|
-
};
|
|
10
|
-
export declare class SettingsRevisionConflictError extends Error {
|
|
11
|
-
constructor();
|
|
12
|
-
}
|
|
13
|
-
export declare function readRawSettingsWithRevision(owner: SettingsOwner): {
|
|
14
|
-
settings: Record<string, unknown> | null;
|
|
15
|
-
revision: string;
|
|
16
|
-
};
|
|
17
|
-
export declare function readRawSettings(owner: SettingsOwner): Record<string, unknown> | null;
|
|
18
|
-
export declare function replaceRawSettings(owner: SettingsOwner, next: Record<string, unknown>, ifRevision: string, validate: (settings: Record<string, unknown>) => void): {
|
|
19
|
-
revision: string;
|
|
20
|
-
changed: boolean;
|
|
21
|
-
};
|
|
22
|
-
export declare function writeRawSettings(owner: SettingsOwner, raw: Record<string, unknown>): void;
|
|
23
|
-
export declare function mutateRawSettings<T>(owner: SettingsOwner, mutate: (current: Record<string, unknown>) => {
|
|
24
|
-
value: T;
|
|
25
|
-
next: Record<string, unknown>;
|
|
26
|
-
}): T;
|
|
27
|
-
export declare function readBookkeeping(owner: SettingsOwner): ScopeState;
|
|
28
|
-
export declare function writeBookkeeping(owner: SettingsOwner, state: ScopeState): void;
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { prepared as cachedStatement } from '../sqlite-statements.js';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import { openScopedStore, withScopedWrite } from './db.js';
|
|
4
|
-
/**
|
|
5
|
-
* The owner row ties a store to the thing it belongs to. For user and profile
|
|
6
|
-
* owners the recorded root is the guard: those rows share one store, and a row
|
|
7
|
-
* whose root differs belongs to something else. A project store is different —
|
|
8
|
-
* it lives inside the root that opens it (`<root>/operations.db`), so the
|
|
9
|
-
* location is the identity and the recorded root is only a note of where the
|
|
10
|
-
* directory last was. When the directory is moved or renamed the note goes
|
|
11
|
-
* stale; the owner is still the same project, so a write rebinds the note and a
|
|
12
|
-
* read accepts it. Kind and profile still have to match, so a user or profile
|
|
13
|
-
* store is never accepted as a project owner.
|
|
14
|
-
*/
|
|
15
|
-
function ensureOwner(db, owner, access) {
|
|
16
|
-
const row = cachedStatement(db, 'SELECT kind, root, profile_id FROM scope_owners WHERE owner_key = ?').get(owner.key);
|
|
17
|
-
if (!row)
|
|
18
|
-
throw new Error(`scoped SQL owner is missing: ${owner.key}`);
|
|
19
|
-
if (row.kind !== owner.kind || (owner.profileId ?? null) !== row.profile_id) {
|
|
20
|
-
throw new Error(`scoped SQL owner does not match its root: ${owner.key}`);
|
|
21
|
-
}
|
|
22
|
-
if (row.root === owner.root)
|
|
23
|
-
return;
|
|
24
|
-
// A profile row is keyed by its id, checked above; its recorded root is a note of where the folder was. A profile
|
|
25
|
-
// folder that moved (`<app>/memory/profiles/<id>` → `<app>/profiles/<id>`) is rebound by the first write.
|
|
26
|
-
const profileMoved = owner.kind === 'profile' && owner.profileId !== undefined;
|
|
27
|
-
if (!profileMoved && (owner.kind !== 'project' || owner.store.kind !== 'project' || owner.store.root !== owner.root)) {
|
|
28
|
-
throw new Error(`scoped SQL owner does not match its root: ${owner.key}`);
|
|
29
|
-
}
|
|
30
|
-
if (access === 'write')
|
|
31
|
-
cachedStatement(db, 'UPDATE scope_owners SET root = ? WHERE owner_key = ?').run(owner.root, owner.key);
|
|
32
|
-
}
|
|
33
|
-
function revisionFor(row) {
|
|
34
|
-
return row === undefined ? 'absent' : createHash('sha256').update(row.raw_json).digest('hex');
|
|
35
|
-
}
|
|
36
|
-
function readRawSettingsRow(db, owner) {
|
|
37
|
-
return cachedStatement(db, 'SELECT raw_json FROM scope_settings WHERE owner_key = ?').get(owner.key);
|
|
38
|
-
}
|
|
39
|
-
export class SettingsRevisionConflictError extends Error {
|
|
40
|
-
constructor() {
|
|
41
|
-
super('settings changed since the supplied revision');
|
|
42
|
-
this.name = 'SettingsRevisionConflictError';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
export function readRawSettingsWithRevision(owner) {
|
|
46
|
-
const db = openScopedStore(owner.store);
|
|
47
|
-
ensureOwner(db, owner, 'read');
|
|
48
|
-
const row = readRawSettingsRow(db, owner);
|
|
49
|
-
return { revision: revisionFor(row), settings: row === undefined ? null : JSON.parse(row.raw_json) };
|
|
50
|
-
}
|
|
51
|
-
export function readRawSettings(owner) {
|
|
52
|
-
return readRawSettingsWithRevision(owner).settings;
|
|
53
|
-
}
|
|
54
|
-
export function replaceRawSettings(owner, next, ifRevision, validate) {
|
|
55
|
-
return withScopedWrite(owner.store, (db) => {
|
|
56
|
-
ensureOwner(db, owner, 'write');
|
|
57
|
-
const current = readRawSettingsRow(db, owner);
|
|
58
|
-
if (ifRevision !== revisionFor(current))
|
|
59
|
-
throw new SettingsRevisionConflictError();
|
|
60
|
-
validate(next);
|
|
61
|
-
const rawJson = JSON.stringify(next);
|
|
62
|
-
if (current?.raw_json === rawJson)
|
|
63
|
-
return { revision: revisionFor(current), changed: false };
|
|
64
|
-
cachedStatement(db, `INSERT INTO scope_settings(owner_key, raw_json) VALUES (?, ?) ON CONFLICT(owner_key) DO UPDATE SET raw_json=excluded.raw_json`).run(owner.key, rawJson);
|
|
65
|
-
return { revision: revisionFor({ raw_json: rawJson }), changed: true };
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
export function writeRawSettings(owner, raw) {
|
|
69
|
-
withScopedWrite(owner.store, (db) => {
|
|
70
|
-
ensureOwner(db, owner, 'write');
|
|
71
|
-
cachedStatement(db, `INSERT INTO scope_settings(owner_key, raw_json) VALUES (?, ?) ON CONFLICT(owner_key) DO UPDATE SET raw_json=excluded.raw_json`).run(owner.key, JSON.stringify(raw));
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
export function mutateRawSettings(owner, mutate) {
|
|
75
|
-
return withScopedWrite(owner.store, (db) => {
|
|
76
|
-
ensureOwner(db, owner, 'write');
|
|
77
|
-
const row = cachedStatement(db, 'SELECT raw_json FROM scope_settings WHERE owner_key = ?').get(owner.key);
|
|
78
|
-
const result = mutate(row ? JSON.parse(row.raw_json) : {});
|
|
79
|
-
cachedStatement(db, `INSERT INTO scope_settings(owner_key, raw_json) VALUES (?, ?) ON CONFLICT(owner_key) DO UPDATE SET raw_json=excluded.raw_json`).run(owner.key, JSON.stringify(result.next));
|
|
80
|
-
return result.value;
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
export function readBookkeeping(owner) {
|
|
84
|
-
const db = openScopedStore(owner.store);
|
|
85
|
-
ensureOwner(db, owner, 'read');
|
|
86
|
-
const bookkeeping = cachedStatement(db, 'SELECT last_self_check, active_canvas FROM scope_bookkeeping WHERE owner_key = ?').get(owner.key);
|
|
87
|
-
const marketplaces = Object.fromEntries(cachedStatement(db, 'SELECT name, last_updated FROM scope_marketplaces WHERE owner_key = ?').all(owner.key).map((row) => [row.name, row.last_updated === null ? {} : { last_updated: row.last_updated }]));
|
|
88
|
-
const plugins = Object.fromEntries(cachedStatement(db, 'SELECT name, last_updated, etag, checked_at FROM scope_plugins WHERE owner_key = ?').all(owner.key).map((row) => [row.name, { ...(row.last_updated === null ? {} : { last_updated: row.last_updated }), ...(row.etag === null ? {} : { etag: row.etag }), ...(row.checked_at === null ? {} : { checked_at: row.checked_at }) }]));
|
|
89
|
-
return { marketplaces, plugins, ...(bookkeeping?.last_self_check === null || bookkeeping === undefined ? {} : { last_self_check: bookkeeping.last_self_check }), activeCanvas: bookkeeping?.active_canvas ?? null };
|
|
90
|
-
}
|
|
91
|
-
export function writeBookkeeping(owner, state) {
|
|
92
|
-
withScopedWrite(owner.store, (db) => {
|
|
93
|
-
ensureOwner(db, owner, 'write');
|
|
94
|
-
cachedStatement(db, `INSERT INTO scope_bookkeeping(owner_key, last_self_check, active_canvas) VALUES (?, ?, ?) ON CONFLICT(owner_key) DO UPDATE SET last_self_check=excluded.last_self_check, active_canvas=excluded.active_canvas`).run(owner.key, state.last_self_check ?? null, state.activeCanvas ?? null);
|
|
95
|
-
cachedStatement(db, 'DELETE FROM scope_marketplaces WHERE owner_key = ?').run(owner.key);
|
|
96
|
-
cachedStatement(db, 'DELETE FROM scope_plugins WHERE owner_key = ?').run(owner.key);
|
|
97
|
-
const market = cachedStatement(db, 'INSERT INTO scope_marketplaces(owner_key, name, last_updated) VALUES (?, ?, ?)');
|
|
98
|
-
for (const [name, value] of Object.entries(state.marketplaces))
|
|
99
|
-
market.run(owner.key, name, value.last_updated ?? null);
|
|
100
|
-
const plugin = cachedStatement(db, 'INSERT INTO scope_plugins(owner_key, name, last_updated, etag, checked_at) VALUES (?, ?, ?, ?, ?)');
|
|
101
|
-
for (const [name, value] of Object.entries(state.plugins))
|
|
102
|
-
plugin.run(owner.key, name, value.last_updated ?? null, value.etag ?? null, value.checked_at ?? null);
|
|
103
|
-
});
|
|
104
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import type { DatabaseSync, StatementSync } from 'node:sqlite';
|
|
2
|
-
export declare function prepared(db: DatabaseSync, sql: string): StatementSync;
|
|
3
|
-
/** Explicitly release compiled statements before closing or migrating a connection. */
|
|
4
|
-
export declare function forgetPrepared(db: DatabaseSync): void;
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/** Statements belong to a connection, not a database path. Cache only fixed SQL:
|
|
2
|
-
* generated IN lists and schema-dependent SQL remain uncached at their call sites.
|
|
3
|
-
*/
|
|
4
|
-
const statements = new WeakMap();
|
|
5
|
-
export function prepared(db, sql) {
|
|
6
|
-
let byDb = statements.get(db);
|
|
7
|
-
if (!byDb)
|
|
8
|
-
statements.set(db, byDb = new Map());
|
|
9
|
-
let statement = byDb.get(sql);
|
|
10
|
-
if (!statement)
|
|
11
|
-
byDb.set(sql, statement = db.prepare(sql));
|
|
12
|
-
return statement;
|
|
13
|
-
}
|
|
14
|
-
/** Explicitly release compiled statements before closing or migrating a connection. */
|
|
15
|
-
export function forgetPrepared(db) {
|
|
16
|
-
statements.delete(db);
|
|
17
|
-
}
|