vellum 0.2.13 → 0.2.14
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/README.md +32 -0
- package/bun.lock +2 -2
- package/docs/skills.md +4 -4
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +213 -3
- package/src/__tests__/app-git-history.test.ts +176 -0
- package/src/__tests__/app-git-service.test.ts +169 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +315 -0
- package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +8 -8
- package/src/__tests__/browser-skill-endstate.test.ts +6 -6
- package/src/__tests__/call-bridge.test.ts +105 -13
- package/src/__tests__/call-domain.test.ts +163 -0
- package/src/__tests__/call-orchestrator.test.ts +113 -0
- package/src/__tests__/call-routes-http.test.ts +246 -6
- package/src/__tests__/channel-approval-routes.test.ts +438 -0
- package/src/__tests__/channel-approval.test.ts +266 -0
- package/src/__tests__/channel-approvals.test.ts +393 -0
- package/src/__tests__/channel-delivery-store.test.ts +447 -0
- package/src/__tests__/checker.test.ts +607 -1048
- package/src/__tests__/cli.test.ts +1 -56
- package/src/__tests__/config-schema.test.ts +137 -18
- package/src/__tests__/conflict-intent-tokenization.test.ts +141 -0
- package/src/__tests__/conflict-policy.test.ts +121 -0
- package/src/__tests__/conflict-store.test.ts +2 -0
- package/src/__tests__/contacts-tools.test.ts +3 -3
- package/src/__tests__/contradiction-checker.test.ts +99 -1
- package/src/__tests__/credential-security-invariants.test.ts +22 -6
- package/src/__tests__/credential-vault-unit.test.ts +780 -0
- package/src/__tests__/elevenlabs-client.test.ts +62 -0
- package/src/__tests__/ephemeral-permissions.test.ts +73 -23
- package/src/__tests__/filesystem-tools.test.ts +579 -0
- package/src/__tests__/gateway-only-enforcement.test.ts +114 -4
- package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +202 -0
- package/src/__tests__/handlers-cu-observation-blob.test.ts +2 -1
- package/src/__tests__/handlers-ipc-blob-probe.test.ts +2 -1
- package/src/__tests__/handlers-slack-config.test.ts +2 -1
- package/src/__tests__/handlers-telegram-config.test.ts +855 -0
- package/src/__tests__/handlers-twitter-config.test.ts +141 -1
- package/src/__tests__/hooks-runner.test.ts +6 -2
- package/src/__tests__/host-file-edit-tool.test.ts +124 -0
- package/src/__tests__/host-file-read-tool.test.ts +62 -0
- package/src/__tests__/host-file-write-tool.test.ts +59 -0
- package/src/__tests__/host-shell-tool.test.ts +251 -0
- package/src/__tests__/ingress-reconcile.test.ts +581 -0
- package/src/__tests__/ipc-snapshot.test.ts +100 -41
- package/src/__tests__/ipc-validate.test.ts +50 -0
- package/src/__tests__/key-migration.test.ts +23 -0
- package/src/__tests__/memory-regressions.test.ts +99 -0
- package/src/__tests__/memory-retrieval.benchmark.test.ts +1 -1
- package/src/__tests__/oauth-callback-registry.test.ts +11 -4
- package/src/__tests__/playbook-execution.test.ts +502 -0
- package/src/__tests__/playbook-tools.test.ts +4 -6
- package/src/__tests__/public-ingress-urls.test.ts +34 -0
- package/src/__tests__/qdrant-manager.test.ts +267 -0
- package/src/__tests__/recurrence-engine-rruleset.test.ts +97 -0
- package/src/__tests__/recurrence-engine.test.ts +9 -0
- package/src/__tests__/recurrence-types.test.ts +8 -0
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/runtime-runs.test.ts +1 -25
- package/src/__tests__/schedule-store.test.ts +16 -14
- package/src/__tests__/schedule-tools.test.ts +83 -0
- package/src/__tests__/scheduler-recurrence.test.ts +111 -10
- package/src/__tests__/secret-allowlist.test.ts +18 -17
- package/src/__tests__/secret-ingress-handler.test.ts +11 -0
- package/src/__tests__/secret-scanner.test.ts +43 -0
- package/src/__tests__/session-conflict-gate.test.ts +442 -6
- package/src/__tests__/session-init.benchmark.test.ts +3 -0
- package/src/__tests__/session-process-bridge.test.ts +242 -0
- package/src/__tests__/session-skill-tools.test.ts +1 -1
- package/src/__tests__/shell-identity.test.ts +256 -0
- package/src/__tests__/skill-projection.benchmark.test.ts +11 -1
- package/src/__tests__/subagent-tools.test.ts +637 -54
- package/src/__tests__/task-management-tools.test.ts +936 -0
- package/src/__tests__/task-runner.test.ts +2 -2
- package/src/__tests__/terminal-tools.test.ts +840 -0
- package/src/__tests__/tool-executor-shell-integration.test.ts +301 -0
- package/src/__tests__/tool-executor.test.ts +85 -151
- package/src/__tests__/tool-permission-simulate-handler.test.ts +336 -0
- package/src/__tests__/trust-store.test.ts +27 -453
- package/src/__tests__/twilio-provider.test.ts +153 -3
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +375 -0
- package/src/__tests__/twilio-routes-twiml.test.ts +4 -4
- package/src/__tests__/twilio-routes.test.ts +17 -262
- package/src/__tests__/twitter-auth-handler.test.ts +2 -1
- package/src/__tests__/twitter-cli-error-shaping.test.ts +208 -0
- package/src/__tests__/twitter-cli-routing.test.ts +252 -0
- package/src/__tests__/twitter-oauth-client.test.ts +209 -0
- package/src/__tests__/workspace-policy.test.ts +213 -0
- package/src/calls/call-bridge.ts +92 -19
- package/src/calls/call-domain.ts +157 -5
- package/src/calls/call-orchestrator.ts +93 -7
- package/src/calls/call-store.ts +6 -0
- package/src/calls/elevenlabs-client.ts +8 -0
- package/src/calls/elevenlabs-config.ts +7 -5
- package/src/calls/twilio-provider.ts +91 -0
- package/src/calls/twilio-routes.ts +32 -37
- package/src/calls/types.ts +3 -1
- package/src/calls/voice-quality.ts +29 -7
- package/src/cli/twitter.ts +200 -21
- package/src/cli.ts +1 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +52 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +55 -4
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +61 -4
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/messaging/tools/messaging-reply.ts +4 -1
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -1
- package/src/config/bundled-skills/messaging/tools/shared.ts +5 -0
- package/src/config/bundled-skills/phone-calls/SKILL.md +142 -34
- package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +95 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +51 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +73 -6
- package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +110 -6
- package/src/config/bundled-skills/public-ingress/SKILL.md +22 -5
- package/src/config/bundled-skills/twitter/SKILL.md +103 -17
- package/src/config/defaults.ts +10 -4
- package/src/config/schema.ts +80 -21
- package/src/config/types.ts +1 -0
- package/src/config/vellum-skills/telegram-setup/SKILL.md +56 -61
- package/src/daemon/assistant-attachments.ts +4 -2
- package/src/daemon/handlers/apps.ts +69 -0
- package/src/daemon/handlers/config.ts +543 -24
- package/src/daemon/handlers/index.ts +1 -0
- package/src/daemon/handlers/sessions.ts +22 -6
- package/src/daemon/handlers/shared.ts +2 -1
- package/src/daemon/handlers/skills.ts +5 -20
- package/src/daemon/ipc-contract-inventory.json +28 -0
- package/src/daemon/ipc-contract.ts +168 -10
- package/src/daemon/ipc-validate.ts +17 -0
- package/src/daemon/lifecycle.ts +2 -0
- package/src/daemon/server.ts +78 -72
- package/src/daemon/session-attachments.ts +1 -1
- package/src/daemon/session-conflict-gate.ts +62 -6
- package/src/daemon/session-notifiers.ts +1 -1
- package/src/daemon/session-process.ts +62 -3
- package/src/daemon/session-tool-setup.ts +1 -2
- package/src/daemon/tls-certs.ts +189 -0
- package/src/daemon/video-thumbnail.ts +5 -3
- package/src/hooks/manager.ts +5 -9
- package/src/memory/app-git-service.ts +295 -0
- package/src/memory/app-store.ts +21 -0
- package/src/memory/conflict-intent.ts +47 -4
- package/src/memory/conflict-policy.ts +73 -0
- package/src/memory/conflict-store.ts +9 -1
- package/src/memory/contradiction-checker.ts +28 -0
- package/src/memory/conversation-key-store.ts +15 -0
- package/src/memory/db.ts +81 -0
- package/src/memory/embedding-local.ts +3 -13
- package/src/memory/external-conversation-store.ts +234 -0
- package/src/memory/job-handlers/conflict.ts +22 -2
- package/src/memory/jobs-worker.ts +67 -28
- package/src/memory/runs-store.ts +54 -7
- package/src/memory/schema.ts +20 -0
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/telegram-bot/adapter.ts +162 -0
- package/src/messaging/providers/telegram-bot/client.ts +104 -0
- package/src/messaging/providers/telegram-bot/types.ts +15 -0
- package/src/messaging/registry.ts +1 -0
- package/src/permissions/checker.ts +48 -44
- package/src/permissions/prompter.ts +0 -4
- package/src/permissions/shell-identity.ts +227 -0
- package/src/permissions/trust-store.ts +76 -53
- package/src/permissions/types.ts +0 -19
- package/src/permissions/workspace-policy.ts +114 -0
- package/src/providers/retry.ts +12 -37
- package/src/runtime/assistant-event-hub.ts +41 -4
- package/src/runtime/channel-approval-parser.ts +60 -0
- package/src/runtime/channel-approval-types.ts +71 -0
- package/src/runtime/channel-approvals.ts +145 -0
- package/src/runtime/gateway-client.ts +16 -0
- package/src/runtime/http-server.ts +29 -9
- package/src/runtime/routes/call-routes.ts +52 -2
- package/src/runtime/routes/channel-routes.ts +296 -16
- package/src/runtime/routes/events-routes.ts +97 -28
- package/src/runtime/routes/run-routes.ts +2 -7
- package/src/runtime/run-orchestrator.ts +0 -3
- package/src/schedule/recurrence-engine.ts +26 -2
- package/src/schedule/recurrence-types.ts +1 -1
- package/src/schedule/schedule-store.ts +12 -3
- package/src/security/secret-scanner.ts +7 -0
- package/src/tasks/ephemeral-permissions.ts +0 -2
- package/src/tasks/task-scheduler.ts +2 -1
- package/src/tools/calls/call-start.ts +8 -0
- package/src/tools/execution-target.ts +21 -0
- package/src/tools/execution-timeout.ts +49 -0
- package/src/tools/executor.ts +6 -135
- package/src/tools/network/web-search.ts +9 -32
- package/src/tools/policy-context.ts +29 -0
- package/src/tools/schedule/update.ts +8 -1
- package/src/tools/terminal/parser.ts +16 -18
- package/src/tools/types.ts +4 -11
- package/src/twitter/oauth-client.ts +102 -0
- package/src/twitter/router.ts +101 -0
- package/src/util/debounce.ts +88 -0
- package/src/util/network-info.ts +47 -0
- package/src/util/platform.ts +29 -4
- package/src/util/promise-guard.ts +37 -0
- package/src/util/retry.ts +98 -0
- package/src/util/truncate.ts +1 -1
- package/src/workspace/git-service.ts +129 -112
- package/src/tools/contacts/contact-merge.ts +0 -55
- package/src/tools/contacts/contact-search.ts +0 -58
- package/src/tools/contacts/contact-upsert.ts +0 -64
- package/src/tools/playbooks/index.ts +0 -4
- package/src/tools/playbooks/playbook-create.ts +0 -96
- package/src/tools/playbooks/playbook-delete.ts +0 -52
- package/src/tools/playbooks/playbook-list.ts +0 -74
- package/src/tools/playbooks/playbook-update.ts +0 -111
|
@@ -4,6 +4,7 @@ import { execFile } from 'node:child_process';
|
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { getLogger } from '../util/logger.js';
|
|
6
6
|
import { getConfig } from '../config/loader.js';
|
|
7
|
+
import { PromiseGuard } from '../util/promise-guard.js';
|
|
7
8
|
|
|
8
9
|
const execFileAsync = promisify(execFile);
|
|
9
10
|
const log = getLogger('workspace-git');
|
|
@@ -133,7 +134,7 @@ export class WorkspaceGitService {
|
|
|
133
134
|
private readonly workspaceDir: string;
|
|
134
135
|
private readonly mutex: Mutex;
|
|
135
136
|
private initialized = false;
|
|
136
|
-
private
|
|
137
|
+
private readonly initGuard = new PromiseGuard<void>();
|
|
137
138
|
private consecutiveFailures = 0;
|
|
138
139
|
private nextAllowedAttemptMs = 0;
|
|
139
140
|
private initConsecutiveFailures = 0;
|
|
@@ -236,81 +237,38 @@ export class WorkspaceGitService {
|
|
|
236
237
|
}
|
|
237
238
|
|
|
238
239
|
// If initialization is in progress, wait for it
|
|
239
|
-
if (this.
|
|
240
|
-
return this.
|
|
240
|
+
if (this.initGuard.active) {
|
|
241
|
+
return this.initGuard.run(() => { throw new Error('unreachable'); });
|
|
241
242
|
}
|
|
242
243
|
|
|
243
244
|
// Circuit breaker: skip if multiple recent init attempts have been failing.
|
|
244
|
-
// Checked AFTER
|
|
245
|
+
// Checked AFTER initGuard.active so callers waiting on in-progress init aren't
|
|
245
246
|
// blocked, and only activates after 2+ consecutive failures so that a
|
|
246
247
|
// single transient failure allows immediate retry.
|
|
247
248
|
if (this.isInitBreakerOpen()) {
|
|
248
249
|
throw new Error('Init circuit breaker open: backing off after repeated failures');
|
|
249
250
|
}
|
|
250
251
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const gitDir = join(this.workspaceDir, '.git');
|
|
259
|
-
|
|
260
|
-
if (existsSync(gitDir)) {
|
|
261
|
-
// Validate existing repo is not corrupted before marking as ready.
|
|
262
|
-
// A corrupted .git directory (e.g. missing HEAD) would cause all
|
|
263
|
-
// subsequent git operations to fail with confusing errors.
|
|
264
|
-
try {
|
|
265
|
-
await this.execGit(['rev-parse', '--git-dir']);
|
|
266
|
-
} catch (err: unknown) {
|
|
267
|
-
// Distinguish transient failures from genuine corruption.
|
|
268
|
-
// Transient errors (timeouts, permissions, missing git binary)
|
|
269
|
-
// should NOT destroy .git — they will resolve on retry via
|
|
270
|
-
// the initPromise clearing logic.
|
|
271
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
272
|
-
const execErr = err as ExecError;
|
|
273
|
-
const isTimeout = execErr.killed === true
|
|
274
|
-
|| execErr.signal === 'SIGTERM'
|
|
275
|
-
|| errMsg.includes('SIGTERM')
|
|
276
|
-
|| errMsg.includes('timed out');
|
|
277
|
-
const isPermission = execErr.code === 'EACCES'
|
|
278
|
-
|| errMsg.includes('EACCES')
|
|
279
|
-
|| errMsg.toLowerCase().includes('permission denied');
|
|
280
|
-
const isMissingBinary = execErr.code === 'ENOENT'
|
|
281
|
-
|| errMsg.includes('ENOENT');
|
|
282
|
-
|
|
283
|
-
if (isTimeout || isPermission || isMissingBinary) {
|
|
284
|
-
// Re-throw so initialization fails gracefully without
|
|
285
|
-
// destroying valid git history.
|
|
286
|
-
throw err;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Genuine corruption (e.g. missing HEAD, broken refs) —
|
|
290
|
-
// remove corrupted .git and fall through to full init below.
|
|
291
|
-
log.warn(
|
|
292
|
-
{ workspaceDir: this.workspaceDir, err: errMsg },
|
|
293
|
-
'Corrupted .git directory detected; reinitializing',
|
|
294
|
-
);
|
|
295
|
-
const { rmSync } = await import('node:fs');
|
|
296
|
-
rmSync(gitDir, { recursive: true, force: true });
|
|
252
|
+
return this.initGuard.run(
|
|
253
|
+
() => this.mutex.withLock(async () => {
|
|
254
|
+
// Double-check after acquiring lock
|
|
255
|
+
if (this.initialized) {
|
|
256
|
+
return;
|
|
297
257
|
}
|
|
298
258
|
|
|
259
|
+
const gitDir = join(this.workspaceDir, '.git');
|
|
260
|
+
|
|
299
261
|
if (existsSync(gitDir)) {
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
// leaves .git present with an undefined HEAD. In that case,
|
|
304
|
-
// fall through to the initial commit logic below.
|
|
305
|
-
let headExists = false;
|
|
262
|
+
// Validate existing repo is not corrupted before marking as ready.
|
|
263
|
+
// A corrupted .git directory (e.g. missing HEAD) would cause all
|
|
264
|
+
// subsequent git operations to fail with confusing errors.
|
|
306
265
|
try {
|
|
307
|
-
await this.execGit(['rev-parse', '
|
|
308
|
-
headExists = true;
|
|
266
|
+
await this.execGit(['rev-parse', '--git-dir']);
|
|
309
267
|
} catch (err: unknown) {
|
|
310
|
-
// Distinguish transient failures from genuine
|
|
268
|
+
// Distinguish transient failures from genuine corruption.
|
|
311
269
|
// Transient errors (timeouts, permissions, missing git binary)
|
|
312
|
-
// should NOT
|
|
313
|
-
//
|
|
270
|
+
// should NOT destroy .git — they will resolve on retry via
|
|
271
|
+
// the guard clearing logic.
|
|
314
272
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
315
273
|
const execErr = err as ExecError;
|
|
316
274
|
const isTimeout = execErr.killed === true
|
|
@@ -324,68 +282,104 @@ export class WorkspaceGitService {
|
|
|
324
282
|
|| errMsg.includes('ENOENT');
|
|
325
283
|
|
|
326
284
|
if (isTimeout || isPermission || isMissingBinary) {
|
|
285
|
+
// Re-throw so initialization fails gracefully without
|
|
286
|
+
// destroying valid git history.
|
|
327
287
|
throw err;
|
|
328
288
|
}
|
|
329
|
-
// Genuine "no commits" (unborn HEAD) — fall through to
|
|
330
|
-
// create the initial commit.
|
|
331
|
-
}
|
|
332
289
|
|
|
333
|
-
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
await this.ensureOnMainLocked();
|
|
342
|
-
this.initialized = true;
|
|
343
|
-
this.recordInitSuccess();
|
|
344
|
-
return;
|
|
290
|
+
// Genuine corruption (e.g. missing HEAD, broken refs) —
|
|
291
|
+
// remove corrupted .git and fall through to full init below.
|
|
292
|
+
log.warn(
|
|
293
|
+
{ workspaceDir: this.workspaceDir, err: errMsg },
|
|
294
|
+
'Corrupted .git directory detected; reinitializing',
|
|
295
|
+
);
|
|
296
|
+
const { rmSync } = await import('node:fs');
|
|
297
|
+
rmSync(gitDir, { recursive: true, force: true });
|
|
345
298
|
}
|
|
346
|
-
}
|
|
347
|
-
// Otherwise fall through to reinitialize / create initial commit
|
|
348
|
-
}
|
|
349
299
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
300
|
+
if (existsSync(gitDir)) {
|
|
301
|
+
// .git exists and passed the corruption check, but we still
|
|
302
|
+
// need to verify that at least one commit exists. A partial
|
|
303
|
+
// init (e.g. git init succeeded but the initial commit failed)
|
|
304
|
+
// leaves .git present with an undefined HEAD. In that case,
|
|
305
|
+
// fall through to the initial commit logic below.
|
|
306
|
+
let headExists = false;
|
|
307
|
+
try {
|
|
308
|
+
await this.execGit(['rev-parse', 'HEAD']);
|
|
309
|
+
headExists = true;
|
|
310
|
+
} catch (err: unknown) {
|
|
311
|
+
// Distinguish transient failures from genuine "no commits".
|
|
312
|
+
// Transient errors (timeouts, permissions, missing git binary)
|
|
313
|
+
// should NOT fall through to re-initialization — they will
|
|
314
|
+
// resolve on retry via the guard clearing logic.
|
|
315
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
316
|
+
const execErr = err as ExecError;
|
|
317
|
+
const isTimeout = execErr.killed === true
|
|
318
|
+
|| execErr.signal === 'SIGTERM'
|
|
319
|
+
|| errMsg.includes('SIGTERM')
|
|
320
|
+
|| errMsg.includes('timed out');
|
|
321
|
+
const isPermission = execErr.code === 'EACCES'
|
|
322
|
+
|| errMsg.includes('EACCES')
|
|
323
|
+
|| errMsg.toLowerCase().includes('permission denied');
|
|
324
|
+
const isMissingBinary = execErr.code === 'ENOENT'
|
|
325
|
+
|| errMsg.includes('ENOENT');
|
|
326
|
+
|
|
327
|
+
if (isTimeout || isPermission || isMissingBinary) {
|
|
328
|
+
throw err;
|
|
329
|
+
}
|
|
330
|
+
// Genuine "no commits" (unborn HEAD) — fall through to
|
|
331
|
+
// create the initial commit.
|
|
332
|
+
}
|
|
368
333
|
|
|
369
|
-
|
|
334
|
+
if (headExists) {
|
|
335
|
+
// HEAD resolves — repo is fully initialized.
|
|
336
|
+
// Run normalization for existing repos that may have been
|
|
337
|
+
// created before these helpers existed, or by external tools.
|
|
338
|
+
// These calls are OUTSIDE the rev-parse try/catch so that
|
|
339
|
+
// normalization errors are not misclassified as "no commits".
|
|
340
|
+
this.ensureGitignoreRulesLocked();
|
|
341
|
+
await this.ensureCommitIdentityLocked();
|
|
342
|
+
await this.ensureOnMainLocked();
|
|
343
|
+
this.initialized = true;
|
|
344
|
+
this.recordInitSuccess();
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Otherwise fall through to reinitialize / create initial commit
|
|
349
|
+
}
|
|
370
350
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
351
|
+
// Initialize new git repository
|
|
352
|
+
await this.execGit(['init', '-b', 'main']);
|
|
353
|
+
|
|
354
|
+
// Run normalization (gitignore + identity + branch enforcement).
|
|
355
|
+
// For fresh `git init -b main` the branch is already main, but
|
|
356
|
+
// in the corruption-recovery path we fall through here after
|
|
357
|
+
// removing .git, so branch enforcement is still useful.
|
|
358
|
+
this.ensureGitignoreRulesLocked();
|
|
359
|
+
await this.ensureCommitIdentityLocked();
|
|
360
|
+
await this.ensureOnMainLocked();
|
|
361
|
+
|
|
362
|
+
// Create initial commit synchronously within the lock to prevent
|
|
363
|
+
// races with the first commitChanges() call. Without this, the
|
|
364
|
+
// initial commit could run concurrently and consume edits meant
|
|
365
|
+
// for the first user-requested commit.
|
|
366
|
+
const status = await this.getStatusInternal();
|
|
367
|
+
const hasExistingFiles = status.untracked.length > 1 || // More than just .gitignore
|
|
368
|
+
status.untracked.some(f => f !== '.gitignore');
|
|
374
369
|
|
|
375
|
-
|
|
370
|
+
await this.execGit(['add', '-A']);
|
|
376
371
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
372
|
+
const message = hasExistingFiles
|
|
373
|
+
? 'Initial commit: migrated existing workspace'
|
|
374
|
+
: 'Initial commit: new workspace';
|
|
380
375
|
|
|
381
|
-
|
|
382
|
-
// calls can retry instead of permanently returning the rejected promise.
|
|
383
|
-
this.initPromise.catch(() => {
|
|
384
|
-
this.initPromise = null;
|
|
385
|
-
this.recordInitFailure();
|
|
386
|
-
});
|
|
376
|
+
await this.execGit(['commit', '-m', message, '--allow-empty']);
|
|
387
377
|
|
|
388
|
-
|
|
378
|
+
this.initialized = true;
|
|
379
|
+
this.recordInitSuccess();
|
|
380
|
+
}),
|
|
381
|
+
() => this.recordInitFailure(),
|
|
382
|
+
);
|
|
389
383
|
}
|
|
390
384
|
|
|
391
385
|
/**
|
|
@@ -732,6 +726,29 @@ export class WorkspaceGitService {
|
|
|
732
726
|
}
|
|
733
727
|
}
|
|
734
728
|
|
|
729
|
+
/**
|
|
730
|
+
* Run an arbitrary read-only git command in the workspace directory.
|
|
731
|
+
* Uses the same clean env and timeout as other git operations.
|
|
732
|
+
* Does NOT acquire the mutex — callers must ensure they are not
|
|
733
|
+
* writing to the repo concurrently (or accept eventual-consistency).
|
|
734
|
+
*/
|
|
735
|
+
async runReadOnlyGit(args: string[]): Promise<{ stdout: string; stderr: string }> {
|
|
736
|
+
await this.ensureInitialized();
|
|
737
|
+
return this.execGit(args);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Run a sequence of git commands atomically under the workspace mutex.
|
|
742
|
+
* Use this for write operations that need serialization with other
|
|
743
|
+
* git mutations (e.g. checkout + commit).
|
|
744
|
+
*/
|
|
745
|
+
async runWithMutex(fn: (exec: (args: string[]) => Promise<{ stdout: string; stderr: string }>) => Promise<void>): Promise<void> {
|
|
746
|
+
await this.ensureInitialized();
|
|
747
|
+
await this.mutex.withLock(async () => {
|
|
748
|
+
await fn((args) => this.execGit(args));
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
735
752
|
/**
|
|
736
753
|
* Get the commit hash of the current HEAD.
|
|
737
754
|
* This is a lightweight read-only operation that does not require the mutex.
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { ToolContext, ToolExecutionResult } from '../types.js';
|
|
2
|
-
import { mergeContacts, getContact } from '../../contacts/contact-store.js';
|
|
3
|
-
|
|
4
|
-
export async function executeContactMerge(
|
|
5
|
-
input: Record<string, unknown>,
|
|
6
|
-
_context: ToolContext,
|
|
7
|
-
): Promise<ToolExecutionResult> {
|
|
8
|
-
const keepId = input.keep_id as string | undefined;
|
|
9
|
-
const mergeId = input.merge_id as string | undefined;
|
|
10
|
-
|
|
11
|
-
if (!keepId || typeof keepId !== 'string') {
|
|
12
|
-
return { content: 'Error: keep_id is required', isError: true };
|
|
13
|
-
}
|
|
14
|
-
if (!mergeId || typeof mergeId !== 'string') {
|
|
15
|
-
return { content: 'Error: merge_id is required', isError: true };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Show what will be merged for clarity
|
|
19
|
-
const keepContact = getContact(keepId);
|
|
20
|
-
const mergeContact = getContact(mergeId);
|
|
21
|
-
|
|
22
|
-
if (!keepContact) {
|
|
23
|
-
return { content: `Error: Contact "${keepId}" not found`, isError: true };
|
|
24
|
-
}
|
|
25
|
-
if (!mergeContact) {
|
|
26
|
-
return { content: `Error: Contact "${mergeId}" not found`, isError: true };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const merged = mergeContacts(keepId, mergeId);
|
|
31
|
-
|
|
32
|
-
const channelList = merged.channels
|
|
33
|
-
.map((ch) => ` - ${ch.type}: ${ch.address}${ch.isPrimary ? ' (primary)' : ''}`)
|
|
34
|
-
.join('\n');
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
content: [
|
|
38
|
-
`Merged "${mergeContact.displayName}" into "${keepContact.displayName}".`,
|
|
39
|
-
``,
|
|
40
|
-
`Surviving contact (${merged.id}):`,
|
|
41
|
-
` Name: ${merged.displayName}`,
|
|
42
|
-
` Importance: ${merged.importance.toFixed(2)}`,
|
|
43
|
-
` Interactions: ${merged.interactionCount}`,
|
|
44
|
-
merged.relationship ? ` Relationship: ${merged.relationship}` : null,
|
|
45
|
-
merged.channels.length > 0 ? ` Channels:\n${channelList}` : null,
|
|
46
|
-
``,
|
|
47
|
-
`Deleted contact: ${mergeContact.displayName} (${mergeId})`,
|
|
48
|
-
].filter(Boolean).join('\n'),
|
|
49
|
-
isError: false,
|
|
50
|
-
};
|
|
51
|
-
} catch (err) {
|
|
52
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
53
|
-
return { content: `Error: ${msg}`, isError: true };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type { ToolContext, ToolExecutionResult } from '../types.js';
|
|
2
|
-
import { searchContacts } from '../../contacts/contact-store.js';
|
|
3
|
-
import type { ContactWithChannels } from '../../contacts/types.js';
|
|
4
|
-
|
|
5
|
-
function formatContactSummary(c: ContactWithChannels): string {
|
|
6
|
-
const parts = [`- **${c.displayName}** (ID: ${c.id})`];
|
|
7
|
-
if (c.relationship) parts.push(` Relationship: ${c.relationship}`);
|
|
8
|
-
parts.push(` Importance: ${c.importance.toFixed(2)} | Interactions: ${c.interactionCount}`);
|
|
9
|
-
if (c.channels.length > 0) {
|
|
10
|
-
const channelList = c.channels
|
|
11
|
-
.map((ch) => `${ch.type}:${ch.address}${ch.isPrimary ? '*' : ''}`)
|
|
12
|
-
.join(', ');
|
|
13
|
-
parts.push(` Channels: ${channelList}`);
|
|
14
|
-
}
|
|
15
|
-
return parts.join('\n');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function executeContactSearch(
|
|
19
|
-
input: Record<string, unknown>,
|
|
20
|
-
_context: ToolContext,
|
|
21
|
-
): Promise<ToolExecutionResult> {
|
|
22
|
-
const query = input.query as string | undefined;
|
|
23
|
-
const channelAddress = input.channel_address as string | undefined;
|
|
24
|
-
const channelType = input.channel_type as string | undefined;
|
|
25
|
-
const relationship = input.relationship as string | undefined;
|
|
26
|
-
const limit = input.limit as number | undefined;
|
|
27
|
-
|
|
28
|
-
if (!query && !channelAddress && !relationship) {
|
|
29
|
-
return {
|
|
30
|
-
content: 'Error: At least one search criterion is required (query, channel_address, or relationship)',
|
|
31
|
-
isError: true,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const results = searchContacts({
|
|
37
|
-
query,
|
|
38
|
-
channelAddress,
|
|
39
|
-
channelType,
|
|
40
|
-
relationship,
|
|
41
|
-
limit,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
if (results.length === 0) {
|
|
45
|
-
return { content: 'No contacts found matching the search criteria.', isError: false };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const lines = [`Found ${results.length} contact(s):\n`];
|
|
49
|
-
for (const contact of results) {
|
|
50
|
-
lines.push(formatContactSummary(contact));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return { content: lines.join('\n'), isError: false };
|
|
54
|
-
} catch (err) {
|
|
55
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
56
|
-
return { content: `Error: ${msg}`, isError: true };
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import type { ToolContext, ToolExecutionResult } from '../types.js';
|
|
2
|
-
import { upsertContact } from '../../contacts/contact-store.js';
|
|
3
|
-
|
|
4
|
-
function formatContact(c: ReturnType<typeof upsertContact>): string {
|
|
5
|
-
const lines = [
|
|
6
|
-
`Contact ${c.id}`,
|
|
7
|
-
` Name: ${c.displayName}`,
|
|
8
|
-
];
|
|
9
|
-
if (c.relationship) lines.push(` Relationship: ${c.relationship}`);
|
|
10
|
-
lines.push(` Importance: ${c.importance.toFixed(2)}`);
|
|
11
|
-
if (c.responseExpectation) lines.push(` Response expectation: ${c.responseExpectation}`);
|
|
12
|
-
if (c.preferredTone) lines.push(` Preferred tone: ${c.preferredTone}`);
|
|
13
|
-
if (c.interactionCount > 0) lines.push(` Interactions: ${c.interactionCount}`);
|
|
14
|
-
if (c.channels.length > 0) {
|
|
15
|
-
lines.push(' Channels:');
|
|
16
|
-
for (const ch of c.channels) {
|
|
17
|
-
const primary = ch.isPrimary ? ' (primary)' : '';
|
|
18
|
-
lines.push(` - ${ch.type}: ${ch.address}${primary}`);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return lines.join('\n');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function executeContactUpsert(
|
|
25
|
-
input: Record<string, unknown>,
|
|
26
|
-
_context: ToolContext,
|
|
27
|
-
): Promise<ToolExecutionResult> {
|
|
28
|
-
const displayName = input.display_name as string | undefined;
|
|
29
|
-
if (!displayName || typeof displayName !== 'string' || displayName.trim().length === 0) {
|
|
30
|
-
return { content: 'Error: display_name is required and must be a non-empty string', isError: true };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const importance = input.importance as number | undefined;
|
|
34
|
-
if (importance !== undefined && (typeof importance !== 'number' || importance < 0 || importance > 1)) {
|
|
35
|
-
return { content: 'Error: importance must be a number between 0 and 1', isError: true };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const rawChannels = input.channels as Array<{ type: string; address: string; is_primary?: boolean }> | undefined;
|
|
39
|
-
const channels = rawChannels?.map((ch) => ({
|
|
40
|
-
type: ch.type,
|
|
41
|
-
address: ch.address,
|
|
42
|
-
isPrimary: ch.is_primary,
|
|
43
|
-
}));
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const contact = upsertContact({
|
|
47
|
-
id: input.id as string | undefined,
|
|
48
|
-
displayName: displayName.trim(),
|
|
49
|
-
relationship: input.relationship as string | undefined,
|
|
50
|
-
importance,
|
|
51
|
-
responseExpectation: input.response_expectation as string | undefined,
|
|
52
|
-
preferredTone: input.preferred_tone as string | undefined,
|
|
53
|
-
channels,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
content: `${contact.created ? 'Created' : 'Updated'} contact:\n${formatContact(contact)}`,
|
|
58
|
-
isError: false,
|
|
59
|
-
};
|
|
60
|
-
} catch (err) {
|
|
61
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
62
|
-
return { content: `Error: ${msg}`, isError: true };
|
|
63
|
-
}
|
|
64
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { and, eq } from 'drizzle-orm';
|
|
2
|
-
import { v4 as uuid } from 'uuid';
|
|
3
|
-
import type { ToolContext, ToolExecutionResult } from '../types.js';
|
|
4
|
-
import { getDb } from '../../memory/db.js';
|
|
5
|
-
import { computeMemoryFingerprint } from '../../memory/fingerprint.js';
|
|
6
|
-
import { memoryItems } from '../../memory/schema.js';
|
|
7
|
-
import { enqueueMemoryJob } from '../../memory/jobs-store.js';
|
|
8
|
-
import type { Playbook, PlaybookAutonomyLevel } from '../../playbooks/types.js';
|
|
9
|
-
import { truncate } from '../../util/truncate.js';
|
|
10
|
-
|
|
11
|
-
const VALID_AUTONOMY_LEVELS = new Set<string>(['auto', 'draft', 'notify']);
|
|
12
|
-
|
|
13
|
-
export async function executePlaybookCreate(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
|
|
14
|
-
const trigger = input.trigger as string;
|
|
15
|
-
const action = input.action as string;
|
|
16
|
-
|
|
17
|
-
if (!trigger || typeof trigger !== 'string') {
|
|
18
|
-
return { content: 'Error: trigger is required and must be a string', isError: true };
|
|
19
|
-
}
|
|
20
|
-
if (!action || typeof action !== 'string') {
|
|
21
|
-
return { content: 'Error: action is required and must be a string', isError: true };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const channel = typeof input.channel === 'string' ? input.channel : '*';
|
|
25
|
-
const category = typeof input.category === 'string' ? input.category : 'general';
|
|
26
|
-
const autonomyLevel: PlaybookAutonomyLevel =
|
|
27
|
-
typeof input.autonomy_level === 'string' && VALID_AUTONOMY_LEVELS.has(input.autonomy_level)
|
|
28
|
-
? (input.autonomy_level as PlaybookAutonomyLevel)
|
|
29
|
-
: 'draft';
|
|
30
|
-
const priority = typeof input.priority === 'number' ? input.priority : 0;
|
|
31
|
-
|
|
32
|
-
const playbook: Playbook = { trigger, channel, category, action, autonomyLevel, priority };
|
|
33
|
-
const statement = JSON.stringify(playbook);
|
|
34
|
-
const subject = truncate(`Playbook: ${trigger}`, 80, '');
|
|
35
|
-
const scopeId = context.memoryScopeId ?? 'default';
|
|
36
|
-
|
|
37
|
-
const fingerprint = computeMemoryFingerprint(scopeId, 'playbook', subject, statement);
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
const db = getDb();
|
|
41
|
-
|
|
42
|
-
const existing = db
|
|
43
|
-
.select()
|
|
44
|
-
.from(memoryItems)
|
|
45
|
-
.where(and(eq(memoryItems.fingerprint, fingerprint), eq(memoryItems.scopeId, scopeId)))
|
|
46
|
-
.get();
|
|
47
|
-
|
|
48
|
-
if (existing) {
|
|
49
|
-
return {
|
|
50
|
-
content: `A playbook with this exact configuration already exists (ID: ${existing.id}).`,
|
|
51
|
-
isError: false,
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const id = uuid();
|
|
56
|
-
const now = Date.now();
|
|
57
|
-
|
|
58
|
-
db.insert(memoryItems).values({
|
|
59
|
-
id,
|
|
60
|
-
kind: 'playbook',
|
|
61
|
-
subject,
|
|
62
|
-
statement,
|
|
63
|
-
status: 'active',
|
|
64
|
-
confidence: 0.95,
|
|
65
|
-
importance: 0.8,
|
|
66
|
-
fingerprint,
|
|
67
|
-
verificationState: 'user_confirmed',
|
|
68
|
-
scopeId,
|
|
69
|
-
firstSeenAt: now,
|
|
70
|
-
lastSeenAt: now,
|
|
71
|
-
lastUsedAt: null,
|
|
72
|
-
}).run();
|
|
73
|
-
|
|
74
|
-
enqueueMemoryJob('embed_item', { itemId: id });
|
|
75
|
-
|
|
76
|
-
const autonomyLabel = autonomyLevel === 'auto' ? 'execute automatically'
|
|
77
|
-
: autonomyLevel === 'draft' ? 'draft for review' : 'notify only';
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
content: [
|
|
81
|
-
'Playbook created successfully.',
|
|
82
|
-
` ID: ${id}`,
|
|
83
|
-
` Trigger: ${trigger}`,
|
|
84
|
-
` Channel: ${channel}`,
|
|
85
|
-
` Category: ${category}`,
|
|
86
|
-
` Action: ${action}`,
|
|
87
|
-
` Autonomy: ${autonomyLabel}`,
|
|
88
|
-
` Priority: ${priority}`,
|
|
89
|
-
].join('\n'),
|
|
90
|
-
isError: false,
|
|
91
|
-
};
|
|
92
|
-
} catch (err) {
|
|
93
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
94
|
-
return { content: `Error creating playbook: ${msg}`, isError: true };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { and, eq } from 'drizzle-orm';
|
|
2
|
-
import type { ToolContext, ToolExecutionResult } from '../types.js';
|
|
3
|
-
import { getDb } from '../../memory/db.js';
|
|
4
|
-
import { memoryItems } from '../../memory/schema.js';
|
|
5
|
-
import { parsePlaybookStatement } from '../../playbooks/types.js';
|
|
6
|
-
|
|
7
|
-
export async function executePlaybookDelete(input: Record<string, unknown>, context: ToolContext): Promise<ToolExecutionResult> {
|
|
8
|
-
const playbookId = input.playbook_id as string;
|
|
9
|
-
if (!playbookId || typeof playbookId !== 'string') {
|
|
10
|
-
return { content: 'Error: playbook_id is required and must be a string', isError: true };
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const scopeId = context.memoryScopeId ?? 'default';
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const db = getDb();
|
|
17
|
-
|
|
18
|
-
const existing = db
|
|
19
|
-
.select()
|
|
20
|
-
.from(memoryItems)
|
|
21
|
-
.where(and(
|
|
22
|
-
eq(memoryItems.id, playbookId),
|
|
23
|
-
eq(memoryItems.kind, 'playbook'),
|
|
24
|
-
eq(memoryItems.scopeId, scopeId),
|
|
25
|
-
))
|
|
26
|
-
.get();
|
|
27
|
-
|
|
28
|
-
if (!existing) {
|
|
29
|
-
return { content: `Error: Playbook with ID "${playbookId}" not found`, isError: true };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const playbook = parsePlaybookStatement(existing.statement);
|
|
33
|
-
const triggerLabel = playbook?.trigger ?? existing.subject;
|
|
34
|
-
|
|
35
|
-
// Soft-delete by marking as superseded rather than hard-deleting,
|
|
36
|
-
// consistent with how other memory items are retired.
|
|
37
|
-
// Setting invalidAt so the cleanup job can eventually hard-delete it.
|
|
38
|
-
const now = Date.now();
|
|
39
|
-
db.update(memoryItems)
|
|
40
|
-
.set({ status: 'superseded', invalidAt: now })
|
|
41
|
-
.where(eq(memoryItems.id, existing.id))
|
|
42
|
-
.run();
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
content: `Playbook deleted (ID: ${existing.id}, trigger: "${triggerLabel}").`,
|
|
46
|
-
isError: false,
|
|
47
|
-
};
|
|
48
|
-
} catch (err) {
|
|
49
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
50
|
-
return { content: `Error deleting playbook: ${msg}`, isError: true };
|
|
51
|
-
}
|
|
52
|
-
}
|