severally-mcp 2.3.1 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.ja.md +52 -17
  2. package/README.md +52 -16
  3. package/bin/severally-mcp.mjs +14 -2
  4. package/docs/install.ja.md +48 -20
  5. package/docs/install.md +48 -21
  6. package/package.json +15 -3
  7. package/plugins/severally/.claude-plugin/plugin.json +1 -1
  8. package/plugins/severally/.codex-plugin/plugin.json +1 -1
  9. package/plugins/severally/dist/severally-mcp.mjs +6573 -5978
  10. package/plugins/severally/skills/_template/reference.md.tmpl +2 -2
  11. package/plugins/severally/skills/antigravity/severally/reference.md +2 -2
  12. package/plugins/severally/skills/claude/severally/reference.md +2 -2
  13. package/plugins/severally/skills/codex/severally/reference.md +2 -2
  14. package/plugins/severally/skills/opencode/severally/reference.md +2 -2
  15. package/scripts/build.mjs +76 -68
  16. package/scripts/doctor.mjs +66 -20
  17. package/scripts/init-config.mjs +8 -6
  18. package/scripts/install.mjs +327 -90
  19. package/scripts/live-check.mjs +62 -23
  20. package/scripts/live-mcp-check.mjs +52 -16
  21. package/scripts/test.mjs +10 -4
  22. package/src/adapters/antigravity-sandbox.mjs +62 -28
  23. package/src/adapters/antigravity.mjs +111 -30
  24. package/src/adapters/claude-code.mjs +121 -28
  25. package/src/adapters/codex.mjs +125 -33
  26. package/src/adapters/opencode-sandbox.mjs +59 -22
  27. package/src/adapters/opencode.mjs +144 -30
  28. package/src/brief.mjs +148 -47
  29. package/src/consultants.mjs +255 -48
  30. package/src/cross-spawn-escape.d.ts +7 -0
  31. package/src/export.mjs +99 -18
  32. package/src/expose-paths.mjs +310 -50
  33. package/src/failures.mjs +31 -14
  34. package/src/jobs.mjs +863 -249
  35. package/src/jsonc.mjs +11 -3
  36. package/src/parse-result.mjs +172 -40
  37. package/src/platform.mjs +56 -8
  38. package/src/policy.mjs +245 -52
  39. package/src/redact.mjs +59 -16
  40. package/src/result-schema.mjs +13 -0
  41. package/src/run.mjs +225 -30
  42. package/src/schema.mjs +132 -27
  43. package/src/server.mjs +178 -81
  44. package/src/status.mjs +94 -13
  45. package/src/store.mjs +418 -35
@@ -56,6 +56,13 @@ import path from 'node:path';
56
56
  import { POLICY, protectedRoots } from '../policy.mjs';
57
57
  import { classifyMessage } from '../failures.mjs';
58
58
 
59
+ /** @typedef {import('../run.mjs').BuildRequest} BuildRequest */
60
+ /** @typedef {import('../run.mjs').ChildResult} ChildResult */
61
+ /** @typedef {import('../run.mjs').Invocation} Invocation */
62
+ /** @typedef {import('../run.mjs').Interpreted} Interpreted */
63
+ /** @typedef {import('../run.mjs').UsageRecord} UsageRecord */
64
+ /** @typedef {import('../run.mjs').Boundary} Boundary */
65
+
59
66
  export const FORBIDDEN_FLAGS = [
60
67
  '--dangerously-bypass-approvals-and-sandbox',
61
68
  '--dangerously-bypass-hook-trust',
@@ -69,13 +76,30 @@ export const FORBIDDEN_FLAGS = [
69
76
  // feature switch that could re-enable what --ignore-user-config drops, and
70
77
  // the modes that leave the read-only sandbox.
71
78
  export const RESERVED_FLAGS = [
72
- '-s', '--sandbox', '--config', '--cd', '--model', '--output-last-message',
73
- '--profile', '-p', '--enable', '--full-auto', '--worktree', '--cyber-access-program',
79
+ '-s',
80
+ '--sandbox',
81
+ '--config',
82
+ '--cd',
83
+ '--model',
84
+ '--output-last-message',
85
+ '--profile',
86
+ '-p',
87
+ '--enable',
88
+ '--full-auto',
89
+ '--worktree',
90
+ '--cyber-access-program',
74
91
  ];
75
92
 
76
93
  const PROFILE = 'severally';
77
94
 
78
- const listDir = (dir) => { try { return fs.readdirSync(dir); } catch { return []; } };
95
+ /** @param {string} dir @returns {string[]} */
96
+ const listDir = (dir) => {
97
+ try {
98
+ return fs.readdirSync(dir);
99
+ } catch {
100
+ return [];
101
+ }
102
+ };
79
103
 
80
104
  /**
81
105
  * The profile's filesystem table, as TOML: everything readable, the protected
@@ -90,15 +114,24 @@ export function filesystemPermissions() {
90
114
  fs.mkdirSync(path.join(POLICY.home, 'homes'), { recursive: true, mode: 0o700 });
91
115
  const codexHome = path.join(os.homedir(), '.codex').replaceAll('\\', '/');
92
116
  const denied = protectedRoots().flatMap((p) => {
93
- if (p === codexHome) return listDir(codexHome).filter((n) => n !== 'packages').map((n) => `${codexHome}/${n}`);
117
+ if (p === codexHome)
118
+ return listDir(codexHome)
119
+ .filter((n) => n !== 'packages')
120
+ .map((n) => `${codexHome}/${n}`);
94
121
  const star = p.indexOf('/*/');
95
122
  if (star === -1) return [p];
96
123
  const [parent, rest] = [p.slice(0, star), p.slice(star + 3)];
97
- return listDir(parent).map((n) => `${parent}/${n}/${rest}`).filter((q) => fs.existsSync(q));
124
+ return listDir(parent)
125
+ .map((n) => `${parent}/${n}/${rest}`)
126
+ .filter((q) => fs.existsSync(q));
98
127
  });
99
128
  return `{ ":root" = "read", ${denied.map((p) => `${JSON.stringify(p)} = "none"`).join(', ')} }`;
100
129
  }
101
130
 
131
+ /**
132
+ * @param {BuildRequest} req
133
+ * @returns {Invocation}
134
+ */
102
135
  export function buildInvocation({ workdir, schemaPath, consultant, network = 'on' }) {
103
136
  const t = POLICY.clis.codex;
104
137
  const chosen = consultant.model;
@@ -106,29 +139,58 @@ export function buildInvocation({ workdir, schemaPath, consultant, network = 'on
106
139
  const args = [
107
140
  'exec',
108
141
  '--json',
109
- '--color', 'never',
110
- '-m', chosen,
111
- '-c', `model_reasoning_effort="${consultant.effort ?? 'high'}"`,
112
- '-c', `tools.web_search=${network !== 'off'}`,
142
+ '--color',
143
+ 'never',
144
+ '-m',
145
+ chosen,
146
+ '-c',
147
+ `model_reasoning_effort="${consultant.effort ?? 'high'}"`,
148
+ '-c',
149
+ `tools.web_search=${network !== 'off'}`,
113
150
  ...(network === 'off' ? ['-c', 'web_search="disabled"'] : []),
114
- '-c', 'hooks.enabled=false',
115
- '-c', 'agents.enabled=false',
116
- '-c', 'shell_environment_policy.inherit="core"',
117
- '-c', 'shell_environment_policy.set={SEVERALLY_ACTIVE="1"}',
151
+ '-c',
152
+ 'hooks.enabled=false',
153
+ '-c',
154
+ 'agents.enabled=false',
155
+ '-c',
156
+ 'shell_environment_policy.inherit="core"',
157
+ '-c',
158
+ 'shell_environment_policy.set={SEVERALLY_ACTIVE="1"}',
118
159
  '--ignore-user-config',
119
160
  '--ignore-rules',
120
161
  '--skip-git-repo-check',
121
162
  '--ephemeral',
122
- '-c', `default_permissions="${PROFILE}"`,
123
- '-c', `permissions.${PROFILE}.filesystem=${filesystemPermissions()}`,
124
- '-C', workdir,
125
- '--output-schema', schemaPath,
126
- '-o', lastMessagePath,
163
+ '-c',
164
+ `default_permissions="${PROFILE}"`,
165
+ '-c',
166
+ `permissions.${PROFILE}.filesystem=${filesystemPermissions()}`,
167
+ '-C',
168
+ workdir,
169
+ '--output-schema',
170
+ schemaPath,
171
+ '-o',
172
+ lastMessagePath,
127
173
  '-',
128
174
  ];
129
175
  return { command: t.cli, args, lastMessagePath, model: chosen };
130
176
  }
131
177
 
178
+ /**
179
+ * One JSONL event from `codex exec --json`, or the item inside it; the CLI
180
+ * controls both, so every field is optional.
181
+ * @typedef {object} CodexItem
182
+ * @property {unknown} [type]
183
+ * @property {unknown} [text]
184
+ * @property {unknown} [message]
185
+ * @property {unknown} [usage]
186
+ * @typedef {CodexItem & { error?: { message?: unknown }, item?: CodexItem }} CodexEvent
187
+ */
188
+
189
+ /**
190
+ * @param {CodexItem | null | undefined} node
191
+ * @param {Record<string, number>} acc
192
+ * @returns {void}
193
+ */
132
194
  function walkUsage(node, acc) {
133
195
  if (!node || typeof node !== 'object') return;
134
196
  if (node.usage && typeof node.usage === 'object') {
@@ -140,24 +202,34 @@ function walkUsage(node, acc) {
140
202
  for (const v of Object.values(node)) if (v && typeof v === 'object') walkUsage(v, acc);
141
203
  }
142
204
 
143
- /** Parse the JSONL event stream `codex exec --json` writes to stdout. */
205
+ /**
206
+ * Parse the JSONL event stream `codex exec --json` writes to stdout.
207
+ * @param {Partial<ChildResult> & { lastMessageText?: string }} run
208
+ * @returns {Interpreted}
209
+ */
144
210
  export function interpret({ stdout, stderr, code, lastMessageText }) {
211
+ /** @type {CodexEvent[]} */
145
212
  const events = [];
146
213
  for (const line of (stdout || '').split('\n')) {
147
214
  const t = line.trim();
148
215
  if (!t.startsWith('{')) continue;
149
- try { events.push(JSON.parse(t)); } catch { /* partial line */ }
216
+ try {
217
+ events.push(JSON.parse(t));
218
+ } catch {
219
+ /* partial line */
220
+ }
150
221
  }
151
222
 
152
223
  const errorEvents = events.filter((e) => e.type === 'error' || e.type === 'turn.failed');
224
+ /** @type {Record<string, number>} */
153
225
  const usage = {};
154
226
  for (const e of events) walkUsage(e, usage);
155
227
 
156
228
  let text = (lastMessageText || '').trim();
157
229
  if (!text) {
158
230
  // Fall back to the last assistant message carried in the event stream.
159
- for (let i = events.length - 1; i >= 0 && !text; i--) {
160
- const e = events[i];
231
+ for (const e of events.toReversed()) {
232
+ if (text) break;
161
233
  const item = e.item ?? e;
162
234
  const candidate =
163
235
  (item && item.type && String(item.type).includes('agent_message') && (item.text ?? item.message)) ||
@@ -167,9 +239,7 @@ export function interpret({ stdout, stderr, code, lastMessageText }) {
167
239
  }
168
240
  }
169
241
 
170
- const errorMsg = errorEvents
171
- .map((e) => e.message ?? e.error?.message ?? JSON.stringify(e))
172
- .join(' | ');
242
+ const errorMsg = errorEvents.map((e) => e.message ?? e.error?.message ?? JSON.stringify(e)).join(' | ');
173
243
  // An error the turn recovered from (the stream still ends in
174
244
  // turn.completed, with an answer) does not throw the answer away; the lead
175
245
  // is told about it instead. A turn that ends in turn.failed has failed,
@@ -201,6 +271,8 @@ export function interpret({ stdout, stderr, code, lastMessageText }) {
201
271
  * What this consultation has done so far. Codex already runs with `exec --json`,
202
272
  * so its event stream is there for the reading -- no flag change and no parser
203
273
  * change, only a use for events we were otherwise discarding.
274
+ * @param {{ stdout?: string }} run
275
+ * @returns {string | null}
204
276
  */
205
277
  export function progress({ stdout }) {
206
278
  const items = [];
@@ -210,7 +282,9 @@ export function progress({ stdout }) {
210
282
  try {
211
283
  const e = JSON.parse(t);
212
284
  if (typeof e?.type === 'string' && e.type.startsWith('item.')) items.push(e);
213
- } catch { /* a partial line, e.g. when the child was killed mid-write */ }
285
+ } catch {
286
+ /* a partial line, e.g. when the child was killed mid-write */
287
+ }
214
288
  }
215
289
  if (!items.length) return null;
216
290
  const last = items[items.length - 1];
@@ -222,14 +296,21 @@ export function progress({ stdout }) {
222
296
  * Tool calls seen in the event stream, counted by item type (and by tool for
223
297
  * MCP and collaboration calls, which is where a sub-agent spawn shows up);
224
298
  * `:error` when the item ended failed or declined.
299
+ * @param {{ stdout?: string }} run
300
+ * @returns {Record<string, number>}
225
301
  */
226
302
  export function toolCalls({ stdout }) {
303
+ /** @type {Map<unknown, string>} */
227
304
  const seen = new Map();
228
305
  for (const line of (stdout || '').split('\n')) {
229
306
  const t = line.trim();
230
307
  if (!t.startsWith('{')) continue;
231
308
  let e;
232
- try { e = JSON.parse(t); } catch { continue; }
309
+ try {
310
+ e = JSON.parse(t);
311
+ } catch {
312
+ continue;
313
+ }
233
314
  const item = e?.item;
234
315
  if (!item?.type || !['item.started', 'item.completed'].includes(e.type)) continue;
235
316
  if (['agent_message', 'reasoning', 'error'].includes(item.type)) continue;
@@ -237,13 +318,22 @@ export function toolCalls({ stdout }) {
237
318
  const name = item.status === 'failed' || item.status === 'declined' ? `${base}:error` : base;
238
319
  seen.set(item.id ?? `${name}#${seen.size}`, name);
239
320
  }
240
- return [...seen.values()].reduce((o, n) => ({ ...o, [n]: (o[n] ?? 0) + 1 }), {});
321
+ return [...seen.values()].reduce((/** @type {Record<string, number>} */ o, n) => ({ ...o, [n]: (o[n] ?? 0) + 1 }), {});
241
322
  }
242
323
 
324
+ /**
325
+ * @param {unknown} raw the usage figures interpret collected from the event stream
326
+ * @returns {UsageRecord}
327
+ */
243
328
  export function usageRecord(raw) {
244
- const u = raw ?? {};
329
+ // interpret's usageRaw is the Record<string, number> walkUsage filled (or null).
330
+ const u = /** @type {Record<string, number>} */ (raw ?? {});
331
+ /** @param {...string} names */
245
332
  const pick = (...names) => {
246
- for (const n of names) if (typeof u[n] === 'number') return u[n];
333
+ for (const n of names) {
334
+ const v = u[n];
335
+ if (typeof v === 'number') return v;
336
+ }
247
337
  return null;
248
338
  };
249
339
  return {
@@ -260,6 +350,8 @@ export function usageRecord(raw) {
260
350
  * What this consultant was launched with, read off the invocation: the
261
351
  * boundary record kept beside each answer. Facts about the launch, not a
262
352
  * claim about what the consultant did.
353
+ * @param {{ args: string[] }} launch
354
+ * @returns {Boundary}
263
355
  */
264
356
  export function boundary({ args }) {
265
357
  return {
@@ -269,9 +361,9 @@ export function boundary({ args }) {
269
361
  writes: false,
270
362
  mcp: false,
271
363
  protected_paths: args.some((a) => a.startsWith(`permissions.${PROFILE}.filesystem=`))
272
- ? 'denied by path (permission profile), as the paths stood at launch: ~/.codex entries other than packages/, '
273
- + 'and legacy job homes, created later are not covered; file names such as .env anywhere are not; on Linux a '
274
- + 'protected file replaced by rename during the run is readable'
364
+ ? 'denied by path (permission profile), as the paths stood at launch: ~/.codex entries other than packages/, ' +
365
+ 'and legacy job homes, created later are not covered; file names such as .env anywhere are not; on Linux a ' +
366
+ 'protected file replaced by rename during the run is readable'
275
367
  : 'not enforced',
276
368
  tools: null,
277
369
  };
@@ -48,8 +48,13 @@ import { sandboxHome } from '../store.mjs';
48
48
  // node:sqlite ships unflagged from Node 23 (and late 22.x); the engines field
49
49
  // allows older runtimes, where credential seeding degrades to a no-op and the
50
50
  // job fails with the CLI's own auth error instead.
51
+ /** @type {typeof import('node:sqlite').DatabaseSync | null} */
51
52
  let DatabaseSync = null;
52
- try { ({ DatabaseSync } = sqliteRequire(import.meta.url)('node:sqlite')); } catch { /* unavailable */ }
53
+ try {
54
+ ({ DatabaseSync } = sqliteRequire(import.meta.url)('node:sqlite'));
55
+ } catch {
56
+ /* unavailable */
57
+ }
53
58
 
54
59
  // Reading is allowed, not denied: every consultant is an equal reader (Codex
55
60
  // runs a read-only shell; Claude Code and Antigravity can read the whole
@@ -93,18 +98,28 @@ const SANDBOX_TOOLS_OFF = Object.freeze({ edit: false, write: false, patch: fals
93
98
  // workdir, and a run that cannot ask refuses -- so this consultant read less
94
99
  // than the others. The last matching rule wins, so "*" comes first.
95
100
  function readRules() {
96
- const deny = [
97
- ...protectedRoots().flatMap((r) => [r, `${r}/**`]),
98
- ...PROTECTED_FILE_NAMES.flatMap((n) => [n, `**/${n}`]),
99
- ];
101
+ const deny = [...protectedRoots().flatMap((r) => [r, `${r}/**`]), ...PROTECTED_FILE_NAMES.flatMap((n) => [n, `**/${n}`])];
100
102
  return { '*': 'allow', ...Object.fromEntries(deny.map((p) => [p, 'deny'])) };
101
103
  }
102
104
 
103
105
  const AUTH_REL = path.join('.local', 'share', 'opencode', 'auth.json');
104
106
 
107
+ /**
108
+ * The synthesised HOME of one consultation, as prepareSandbox returns it.
109
+ * @typedef {object} OpencodeSandbox
110
+ * @property {string} root
111
+ * @property {string} credentials 'copy' or 'missing'
112
+ * @property {string[]} tools
113
+ * @property {string} credentialsSource
114
+ * @property {Record<string, string>} env
115
+ * @property {() => void} cleanup
116
+ * @property {(provider: string) => boolean} seed insert the provider's credential into the session database
117
+ */
118
+
105
119
  /**
106
120
  * Build the synthesised HOME for one consultation.
107
- * @param {{workdir: string}} opts workdir is <jobdir>/work; the home is store.sandboxHome(<job id>).
121
+ * @param {{workdir: string, network?: 'on' | 'off' | undefined}} opts workdir is <jobdir>/work; the home is store.sandboxHome(<job id>).
122
+ * @returns {OpencodeSandbox}
108
123
  */
109
124
  export function prepareSandbox({ workdir, network = 'on' }) {
110
125
  const root = sandboxHome(path.basename(path.dirname(workdir)));
@@ -162,6 +177,7 @@ export function prepareSandbox({ workdir, network = 'on' }) {
162
177
  credentials = 'copy';
163
178
  }
164
179
 
180
+ /** @type {OpencodeSandbox} */
165
181
  const sandbox = {
166
182
  root,
167
183
  credentials,
@@ -180,19 +196,25 @@ export function prepareSandbox({ workdir, network = 'on' }) {
180
196
  // parent directories of the (empty) working directory.
181
197
  OPENCODE_DISABLE_AUTOUPDATE: '1',
182
198
  OPENCODE_DISABLE_PROJECT_CONFIG: '1',
183
- ...(process.platform === 'win32' ? {
184
- USERPROFILE: root,
185
- HOMEDRIVE: path.parse(root).root.replace(/[\\/]$/, ''),
186
- HOMEPATH: root.slice(path.parse(root).root.length - 1),
187
- APPDATA: path.join(root, 'AppData', 'Roaming'),
188
- LOCALAPPDATA: path.join(root, 'AppData', 'Local'),
189
- } : {}),
199
+ ...(process.platform === 'win32'
200
+ ? {
201
+ USERPROFILE: root,
202
+ HOMEDRIVE: path.parse(root).root.replace(/[\\/]$/, ''),
203
+ HOMEPATH: root.slice(path.parse(root).root.length - 1),
204
+ APPDATA: path.join(root, 'AppData', 'Roaming'),
205
+ LOCALAPPDATA: path.join(root, 'AppData', 'Local'),
206
+ }
207
+ : {}),
190
208
  },
191
209
  cleanup() {
192
- try { fs.rmSync(root, { recursive: true, force: true }); } catch { /* best effort */ }
210
+ try {
211
+ fs.rmSync(root, { recursive: true, force: true });
212
+ } catch {
213
+ /* best effort */
214
+ }
193
215
  },
216
+ seed: (provider) => seedCredentialFromAuth(sandbox, provider),
194
217
  };
195
- sandbox.seed = (provider) => seedCredentialFromAuth(sandbox, provider);
196
218
  return sandbox;
197
219
  }
198
220
 
@@ -207,6 +229,8 @@ export function prepareSandbox({ workdir, network = 'on' }) {
207
229
  * -- note "key" (the method), not the "api" auth.json uses. Only api-key
208
230
  * entries are seeded; OAuth tokens have a different shape and lifecycle.
209
231
  *
232
+ * @param {{ root: string }} sandbox
233
+ * @param {unknown} provider
210
234
  * @returns {boolean} true when a row was written and a retry is worth it.
211
235
  */
212
236
  export function seedCredentialFromAuth(sandbox, provider) {
@@ -218,6 +242,7 @@ export function seedCredentialFromAuth(sandbox, provider) {
218
242
  // CLI's own migrations, so an unbootstrapped sandbox is simply not seedable.
219
243
  if (!fs.existsSync(dbPath)) return false;
220
244
 
245
+ /** @type {{ label: import('node:sqlite').SQLInputValue, value: string } | null} */
221
246
  let row = null;
222
247
  // The operator's session database is what the 2.x CLI itself authenticates
223
248
  // with, so it outranks the copied auth.json: an operator who rotated their
@@ -231,7 +256,9 @@ export function seedCredentialFromAuth(sandbox, provider) {
231
256
  try {
232
257
  const db = new DatabaseSync(operatorDb, { readOnly: true });
233
258
  try {
234
- const found = db.prepare('SELECT label, value FROM credential WHERE integration_id = ? ORDER BY time_updated DESC LIMIT 1').get(provider);
259
+ const found = db
260
+ .prepare('SELECT label, value FROM credential WHERE integration_id = ? ORDER BY time_updated DESC LIMIT 1')
261
+ .get(provider);
235
262
  if (found && typeof found.value === 'string') {
236
263
  const parsed = JSON.parse(found.value);
237
264
  if (parsed && typeof parsed === 'object' && typeof parsed.key === 'string' && parsed.key) {
@@ -241,23 +268,33 @@ export function seedCredentialFromAuth(sandbox, provider) {
241
268
  } finally {
242
269
  db.close();
243
270
  }
244
- } catch { /* unreadable or reshaped: try the auth.json source */ }
271
+ } catch {
272
+ /* unreadable or reshaped: try the auth.json source */
273
+ }
245
274
  }
246
275
  if (!row && fs.existsSync(authPath)) {
247
276
  try {
248
277
  const entry = JSON.parse(fs.readFileSync(authPath, 'utf8'))[provider];
249
- if (entry && typeof entry === 'object' && typeof entry.key === 'string' && entry.key
250
- && (!entry.type || entry.type === 'api')) {
278
+ if (
279
+ entry &&
280
+ typeof entry === 'object' &&
281
+ typeof entry.key === 'string' &&
282
+ entry.key &&
283
+ (!entry.type || entry.type === 'api')
284
+ ) {
251
285
  row = { label: 'API key', value: JSON.stringify({ type: 'key', key: entry.key }) };
252
286
  }
253
- } catch { /* unreadable auth.json: nothing seedable */ }
287
+ } catch {
288
+ /* unreadable auth.json: nothing seedable */
289
+ }
254
290
  }
255
291
  if (!row) return false;
256
292
  try {
257
293
  const db = new DatabaseSync(dbPath);
258
294
  try {
259
- db.prepare('INSERT INTO credential (id, integration_id, label, value, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)')
260
- .run(`cred_${crypto.randomBytes(18).toString('base64url')}`, provider, row.label, row.value, Date.now(), Date.now());
295
+ db.prepare(
296
+ 'INSERT INTO credential (id, integration_id, label, value, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)',
297
+ ).run(`cred_${crypto.randomBytes(18).toString('base64url')}`, provider, row.label, row.value, Date.now(), Date.now());
261
298
  } finally {
262
299
  db.close();
263
300
  }