@vellumai/assistant 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +32 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/openapi.yaml +538 -3
- package/package.json +5 -1
- package/src/__tests__/anthropic-provider.test.ts +160 -95
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +47 -1
- package/src/__tests__/app-source-watcher.test.ts +159 -0
- package/src/__tests__/checker.test.ts +38 -6
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
- package/src/__tests__/conversation-agent-loop.test.ts +4 -51
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
- package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
- package/src/__tests__/conversation-wipe.test.ts +2 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
- package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
- package/src/__tests__/date-context.test.ts +76 -210
- package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
- package/src/__tests__/file-list-tool.test.ts +219 -0
- package/src/__tests__/first-greeting.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +180 -3
- package/src/__tests__/identity-routes.test.ts +328 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/install-skill-routing.test.ts +7 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
- package/src/__tests__/llm-context-normalization.test.ts +18 -18
- package/src/__tests__/llm-context-route-provider.test.ts +101 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
- package/src/__tests__/log-export-workspace.test.ts +72 -105
- package/src/__tests__/mcp-abort-signal.test.ts +5 -0
- package/src/__tests__/mcp-client-auth.test.ts +5 -0
- package/src/__tests__/memory-recall-log-store.test.ts +132 -0
- package/src/__tests__/migration-export-streaming.test.ts +304 -0
- package/src/__tests__/migration-import-commit-http.test.ts +11 -10
- package/src/__tests__/mock-fetch.ts +87 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +62 -14
- package/src/__tests__/parser.test.ts +32 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
- package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
- package/src/__tests__/permission-mode-sse.test.ts +418 -0
- package/src/__tests__/permission-mode-store.test.ts +277 -0
- package/src/__tests__/permission-mode.test.ts +101 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
- package/src/__tests__/profiler-routes.test.ts +502 -0
- package/src/__tests__/profiler-run-store.test.ts +441 -0
- package/src/__tests__/proxy-approval-callback.test.ts +4 -75
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/sandbox-host-parity.test.ts +5 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
- package/src/__tests__/search-skills-unified.test.ts +4 -3
- package/src/__tests__/send-endpoint-busy.test.ts +42 -3
- package/src/__tests__/set-permission-mode.test.ts +274 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
- package/src/__tests__/skill-memory.test.ts +2 -783
- package/src/__tests__/strip-memory-injections.test.ts +187 -0
- package/src/__tests__/subagent-detail.test.ts +84 -0
- package/src/__tests__/subagent-disposal.test.ts +308 -0
- package/src/__tests__/subagent-manager-notify.test.ts +19 -10
- package/src/__tests__/subagent-notify-parent.test.ts +390 -0
- package/src/__tests__/subagent-role-registry.test.ts +108 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
- package/src/__tests__/subagent-tools.test.ts +464 -4
- package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
- package/src/__tests__/task-memory-cleanup.test.ts +12 -12
- package/src/__tests__/terminal-tools.test.ts +17 -27
- package/src/__tests__/test-preload.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -26
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/top-level-renderer.test.ts +10 -13
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
- package/src/agent/loop.ts +6 -0
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/cli/__tests__/run-assistant-command.ts +29 -0
- package/src/cli/commands/__tests__/email-download.test.ts +245 -0
- package/src/cli/commands/__tests__/email-list.test.ts +192 -0
- package/src/cli/commands/__tests__/email-register.test.ts +186 -0
- package/src/cli/commands/__tests__/email-send.test.ts +291 -0
- package/src/cli/commands/__tests__/email-status.test.ts +181 -0
- package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
- package/src/cli/commands/__tests__/routes.test.ts +562 -0
- package/src/cli/commands/conversations.ts +1 -8
- package/src/cli/commands/email.ts +584 -835
- package/src/cli/commands/memory.ts +1 -34
- package/src/cli/commands/notifications.ts +7 -2
- package/src/cli/commands/oauth/connect.ts +14 -5
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +2 -0
- package/src/cli.ts +1 -120
- package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
- package/src/config/bundled-skills/gmail/SKILL.md +2 -2
- package/src/config/bundled-skills/messaging/SKILL.md +7 -0
- package/src/config/bundled-skills/schedule/SKILL.md +22 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/SKILL.md +43 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
- package/src/config/env-registry.ts +63 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/filing.ts +51 -0
- package/src/config/schemas/heartbeat.ts +15 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/security.ts +14 -0
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +79 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +158 -65
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-process.ts +13 -7
- package/src/daemon/conversation-runtime-assembly.ts +300 -306
- package/src/daemon/conversation-tool-setup.ts +44 -14
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +18 -0
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +4 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +63 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +199 -157
- package/src/daemon/message-types/conversations.ts +25 -6
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +6 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +89 -9
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/export/transcript-formatter.ts +148 -0
- package/src/filing/filing-service.ts +228 -0
- package/src/heartbeat/heartbeat-service.ts +96 -7
- package/src/mcp/client.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +149 -27
- package/src/memory/admin.ts +33 -32
- package/src/memory/app-store.ts +69 -0
- package/src/memory/conversation-bootstrap.ts +1 -1
- package/src/memory/conversation-crud.ts +136 -107
- package/src/memory/conversation-group-migration.ts +1 -1
- package/src/memory/conversation-queries.ts +58 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/db-init.ts +182 -376
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -15
- package/src/memory/graph/consolidation.ts +38 -4
- package/src/memory/graph/conversation-graph-memory.ts +133 -104
- package/src/memory/graph/extraction-job.ts +9 -4
- package/src/memory/graph/extraction.ts +66 -23
- package/src/memory/graph/graph-memory-state-store.ts +37 -0
- package/src/memory/graph/graph-search.ts +29 -15
- package/src/memory/graph/injection.ts +38 -8
- package/src/memory/graph/inspect.ts +12 -3
- package/src/memory/graph/retriever.ts +365 -262
- package/src/memory/graph/store.test.ts +48 -0
- package/src/memory/graph/store.ts +150 -11
- package/src/memory/graph/tool-handlers.ts +84 -209
- package/src/memory/graph/tools.ts +8 -52
- package/src/memory/graph/types.ts +24 -0
- package/src/memory/job-handlers/cleanup.ts +44 -1
- package/src/memory/jobs-store.ts +70 -60
- package/src/memory/jobs-worker.ts +44 -28
- package/src/memory/llm-request-log-store.ts +96 -12
- package/src/memory/memory-recall-log-store.ts +49 -5
- package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
- package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
- package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
- package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
- package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
- package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +14 -0
- package/src/memory/schema/infrastructure.ts +8 -1
- package/src/memory/schema/memory-core.ts +0 -51
- package/src/memory/schema/memory-graph.ts +15 -0
- package/src/memory/task-memory-cleanup.ts +30 -11
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/permissions/checker.ts +12 -1
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +70 -165
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +25 -4
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/http-server.ts +32 -2
- package/src/runtime/http-types.ts +12 -1
- package/src/runtime/migrations/vbundle-builder.ts +389 -3
- package/src/runtime/migrations/vbundle-importer.ts +8 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
- package/src/runtime/routes/app-management-routes.ts +1 -11
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
- package/src/runtime/routes/archive-utils.ts +29 -0
- package/src/runtime/routes/avatar-routes.ts +2 -9
- package/src/runtime/routes/btw-routes.ts +14 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -14
- package/src/runtime/routes/conversation-query-routes.ts +49 -3
- package/src/runtime/routes/conversation-routes.ts +264 -44
- package/src/runtime/routes/heartbeat-routes.ts +4 -10
- package/src/runtime/routes/identity-routes.ts +53 -18
- package/src/runtime/routes/llm-context-normalization.ts +14 -10
- package/src/runtime/routes/log-export-routes.ts +23 -275
- package/src/runtime/routes/memory-item-routes.test.ts +168 -233
- package/src/runtime/routes/migration-routes.ts +18 -7
- package/src/runtime/routes/profiler-routes.ts +350 -0
- package/src/runtime/routes/schedule-routes.ts +27 -12
- package/src/runtime/routes/settings-routes.ts +95 -8
- package/src/runtime/routes/subagents-routes.ts +28 -7
- package/src/runtime/routes/user-route-dispatcher.ts +223 -0
- package/src/runtime/routes/user-routes.ts +41 -0
- package/src/runtime/routes/workspace-routes.ts +0 -1
- package/src/schedule/schedule-store.ts +30 -0
- package/src/schedule/scheduler.ts +45 -18
- package/src/skills/catalog-install.ts +10 -2
- package/src/skills/managed-store.ts +2 -2
- package/src/skills/skill-memory.ts +1 -293
- package/src/subagent/index.ts +13 -3
- package/src/subagent/manager.ts +308 -29
- package/src/subagent/types.ts +68 -0
- package/src/tasks/task-runner.ts +4 -4
- package/src/tools/apps/executors.ts +29 -4
- package/src/tools/filesystem/list.ts +93 -0
- package/src/tools/permission-checker.ts +78 -0
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/shared/filesystem/errors.ts +5 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
- package/src/tools/shared/filesystem/types.ts +17 -0
- package/src/tools/shared/shell-output.ts +31 -2
- package/src/tools/subagent/abort.ts +12 -2
- package/src/tools/subagent/message.ts +9 -2
- package/src/tools/subagent/notify-parent.ts +79 -0
- package/src/tools/subagent/read.ts +29 -8
- package/src/tools/subagent/resolve.ts +21 -0
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/subagent/status.ts +11 -1
- package/src/tools/system/avatar-generator.ts +3 -3
- package/src/tools/system/register.ts +23 -0
- package/src/tools/system/set-permission-mode.ts +103 -0
- package/src/tools/terminal/parser.ts +30 -5
- package/src/tools/terminal/safe-env.ts +16 -1
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -0
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
- package/src/workspace/migrations/029-seed-pkb.ts +84 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/top-level-renderer.ts +5 -9
- package/src/__tests__/cli-memory.test.ts +0 -377
- package/src/__tests__/clipboard.test.ts +0 -88
- package/src/cli/cli-memory.ts +0 -179
- package/src/util/clipboard.ts +0 -34
|
@@ -9,10 +9,24 @@
|
|
|
9
9
|
* - trust/trust.json: trust rules (optional, lives in protected/ outside workspace)
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { createHash } from "node:crypto";
|
|
13
|
-
import {
|
|
12
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
13
|
+
import {
|
|
14
|
+
closeSync,
|
|
15
|
+
createReadStream,
|
|
16
|
+
createWriteStream,
|
|
17
|
+
existsSync,
|
|
18
|
+
lstatSync,
|
|
19
|
+
openSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
readFileSync,
|
|
22
|
+
readSync,
|
|
23
|
+
} from "node:fs";
|
|
24
|
+
import { stat, unlink } from "node:fs/promises";
|
|
25
|
+
import { tmpdir } from "node:os";
|
|
14
26
|
import { join, relative } from "node:path";
|
|
15
|
-
import {
|
|
27
|
+
import { Readable } from "node:stream";
|
|
28
|
+
import { pipeline } from "node:stream/promises";
|
|
29
|
+
import { createGzip, gzipSync } from "node:zlib";
|
|
16
30
|
|
|
17
31
|
import type {
|
|
18
32
|
ManifestFileEntryType,
|
|
@@ -46,6 +60,12 @@ export interface BuildVBundleResult {
|
|
|
46
60
|
manifest: ManifestType;
|
|
47
61
|
}
|
|
48
62
|
|
|
63
|
+
interface FileMetadata {
|
|
64
|
+
archivePath: string;
|
|
65
|
+
diskPath: string;
|
|
66
|
+
size: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
49
69
|
// ---------------------------------------------------------------------------
|
|
50
70
|
// Hash helpers
|
|
51
71
|
// ---------------------------------------------------------------------------
|
|
@@ -480,3 +500,369 @@ export function buildExportVBundle(
|
|
|
480
500
|
description: description ?? "Runtime export bundle",
|
|
481
501
|
});
|
|
482
502
|
}
|
|
503
|
+
|
|
504
|
+
// ---------------------------------------------------------------------------
|
|
505
|
+
// Streaming export builder — two-pass approach for bounded memory usage
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Walk a directory tree and collect file metadata (paths + sizes) without
|
|
510
|
+
* reading file contents into memory. Uses the same filtering logic as
|
|
511
|
+
* `walkDirectory` (symlink skip, SQLite auxiliary skip, binary detection,
|
|
512
|
+
* skip dirs).
|
|
513
|
+
*/
|
|
514
|
+
function walkDirectoryForMetadata(
|
|
515
|
+
dir: string,
|
|
516
|
+
archivePrefix: string,
|
|
517
|
+
options: WalkDirectoryOptions = {},
|
|
518
|
+
): FileMetadata[] {
|
|
519
|
+
const { includeBinary = false, skipDirs = [] } = options;
|
|
520
|
+
const entries: FileMetadata[] = [];
|
|
521
|
+
|
|
522
|
+
function walk(currentDir: string): void {
|
|
523
|
+
const dirEntries = readdirSync(currentDir, { withFileTypes: true });
|
|
524
|
+
for (const entry of dirEntries) {
|
|
525
|
+
const fullPath = join(currentDir, entry.name);
|
|
526
|
+
|
|
527
|
+
// Skip symlinks
|
|
528
|
+
const fileStat = lstatSync(fullPath);
|
|
529
|
+
if (fileStat.isSymbolicLink()) continue;
|
|
530
|
+
|
|
531
|
+
if (fileStat.isDirectory()) {
|
|
532
|
+
// Check skip list against the relative path from the walk root
|
|
533
|
+
const relDir = relative(dir, fullPath);
|
|
534
|
+
if (skipDirs.some((s) => relDir === s || relDir.startsWith(s + "/"))) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
walk(fullPath);
|
|
538
|
+
} else if (fileStat.isFile()) {
|
|
539
|
+
// Skip SQLite auxiliary files — these are ephemeral and race-prone
|
|
540
|
+
if (
|
|
541
|
+
entry.name.endsWith(".db-wal") ||
|
|
542
|
+
entry.name.endsWith(".db-shm") ||
|
|
543
|
+
entry.name.endsWith(".db-journal")
|
|
544
|
+
) {
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Skip binary files unless explicitly included
|
|
549
|
+
if (!includeBinary) {
|
|
550
|
+
// Read only the first 8 KB to check for null bytes
|
|
551
|
+
const checkLength = Math.min(fileStat.size, 8192);
|
|
552
|
+
if (checkLength > 0) {
|
|
553
|
+
const buf = Buffer.alloc(checkLength);
|
|
554
|
+
const fd = openSync(fullPath, "r");
|
|
555
|
+
try {
|
|
556
|
+
readSync(fd, buf, 0, checkLength, 0);
|
|
557
|
+
} finally {
|
|
558
|
+
closeSync(fd);
|
|
559
|
+
}
|
|
560
|
+
let isBinary = false;
|
|
561
|
+
for (let i = 0; i < checkLength; i++) {
|
|
562
|
+
if (buf[i] === 0) {
|
|
563
|
+
isBinary = true;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (isBinary) continue;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const relativePath = relative(dir, fullPath);
|
|
572
|
+
entries.push({
|
|
573
|
+
archivePath: `${archivePrefix}/${relativePath}`,
|
|
574
|
+
diskPath: fullPath,
|
|
575
|
+
size: fileStat.size,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
walk(dir);
|
|
582
|
+
return entries;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Compute SHA-256 hex digest of a file by streaming — never buffers the
|
|
587
|
+
* entire file in memory. When `size` is provided, only hashes the first
|
|
588
|
+
* `size` bytes to match what will be archived in the tar entry.
|
|
589
|
+
*/
|
|
590
|
+
async function computeFileSha256(
|
|
591
|
+
filePath: string,
|
|
592
|
+
size?: number,
|
|
593
|
+
): Promise<string> {
|
|
594
|
+
const hash = createHash("sha256");
|
|
595
|
+
if (size === 0) return hash.digest("hex");
|
|
596
|
+
const streamOpts =
|
|
597
|
+
size !== undefined ? { start: 0, end: size - 1 } : undefined;
|
|
598
|
+
const stream = createReadStream(filePath, streamOpts);
|
|
599
|
+
for await (const chunk of stream) {
|
|
600
|
+
hash.update(chunk);
|
|
601
|
+
}
|
|
602
|
+
return hash.digest("hex");
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Create just the 512-byte tar header block for a regular file entry.
|
|
607
|
+
* Extracted from `createTarEntry` logic — does NOT include data or padding.
|
|
608
|
+
*/
|
|
609
|
+
function createTarHeaderBlock(name: string, size: number): Uint8Array {
|
|
610
|
+
const encoder = new TextEncoder();
|
|
611
|
+
const nameBytes = encoder.encode(name);
|
|
612
|
+
|
|
613
|
+
const header = new Uint8Array(BLOCK_SIZE);
|
|
614
|
+
|
|
615
|
+
// File name (0-99) — truncated if >100 bytes
|
|
616
|
+
header.set(nameBytes.subarray(0, 100), 0);
|
|
617
|
+
|
|
618
|
+
// File mode (100-107): 0644
|
|
619
|
+
writeOctal(header, 100, 8, 0o644);
|
|
620
|
+
|
|
621
|
+
// Owner ID (108-115)
|
|
622
|
+
writeOctal(header, 108, 8, 0);
|
|
623
|
+
|
|
624
|
+
// Group ID (116-123)
|
|
625
|
+
writeOctal(header, 116, 8, 0);
|
|
626
|
+
|
|
627
|
+
// File size (124-135)
|
|
628
|
+
writeOctal(header, 124, 12, size);
|
|
629
|
+
|
|
630
|
+
// Modification time (136-147)
|
|
631
|
+
writeOctal(header, 136, 12, Math.floor(Date.now() / 1000));
|
|
632
|
+
|
|
633
|
+
// Type flag (156): regular file
|
|
634
|
+
header[156] = "0".charCodeAt(0);
|
|
635
|
+
|
|
636
|
+
// USTAR magic (257-262)
|
|
637
|
+
const magic = encoder.encode("ustar\0");
|
|
638
|
+
header.set(magic, 257);
|
|
639
|
+
|
|
640
|
+
// USTAR version (263-264)
|
|
641
|
+
header[263] = "0".charCodeAt(0);
|
|
642
|
+
header[264] = "0".charCodeAt(0);
|
|
643
|
+
|
|
644
|
+
// Compute and write checksum (148-155)
|
|
645
|
+
const checksum = computeHeaderChecksum(header);
|
|
646
|
+
writeOctal(header, 148, 7, checksum);
|
|
647
|
+
header[155] = 0x20; // trailing space
|
|
648
|
+
|
|
649
|
+
return header;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* If name exceeds 100 bytes, returns the PAX extended header entry
|
|
654
|
+
* concatenated with the regular header block. Otherwise returns just
|
|
655
|
+
* the header block.
|
|
656
|
+
*/
|
|
657
|
+
function createPaxAndHeaderBlocks(name: string, size: number): Uint8Array {
|
|
658
|
+
const encoder = new TextEncoder();
|
|
659
|
+
const nameBytes = encoder.encode(name);
|
|
660
|
+
const needsPax = nameBytes.length > 100;
|
|
661
|
+
|
|
662
|
+
const header = createTarHeaderBlock(name, size);
|
|
663
|
+
|
|
664
|
+
if (needsPax) {
|
|
665
|
+
const paxEntry = createPaxPathEntry(name);
|
|
666
|
+
const result = new Uint8Array(paxEntry.length + header.length);
|
|
667
|
+
result.set(paxEntry, 0);
|
|
668
|
+
result.set(header, paxEntry.length);
|
|
669
|
+
return result;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return header;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Returns zero-filled padding bytes to align data to the tar block boundary.
|
|
677
|
+
*/
|
|
678
|
+
function tarPaddingBytes(dataSize: number): Uint8Array {
|
|
679
|
+
const remainder = dataSize % BLOCK_SIZE;
|
|
680
|
+
if (remainder === 0) return new Uint8Array(0);
|
|
681
|
+
return new Uint8Array(BLOCK_SIZE - remainder);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Async generator that yields raw tar bytes in order:
|
|
686
|
+
* manifest entry, then each file entry, then end-of-archive marker.
|
|
687
|
+
* Each file is streamed from disk — never fully buffered in memory.
|
|
688
|
+
*/
|
|
689
|
+
async function* generateTarStream(
|
|
690
|
+
manifestJson: Uint8Array,
|
|
691
|
+
files: FileMetadata[],
|
|
692
|
+
): AsyncGenerator<Uint8Array> {
|
|
693
|
+
// Manifest entry
|
|
694
|
+
yield createPaxAndHeaderBlocks("manifest.json", manifestJson.length);
|
|
695
|
+
yield manifestJson;
|
|
696
|
+
yield tarPaddingBytes(manifestJson.length);
|
|
697
|
+
|
|
698
|
+
// File entries
|
|
699
|
+
for (const file of files) {
|
|
700
|
+
yield createPaxAndHeaderBlocks(file.archivePath, file.size);
|
|
701
|
+
|
|
702
|
+
// Stream exactly file.size bytes from disk. Capping the read at the
|
|
703
|
+
// declared size keeps the tar structure valid even if the file grows
|
|
704
|
+
// between passes (common for log files on active assistants). If the
|
|
705
|
+
// file shrinks below the declared size, zero-pad to maintain block
|
|
706
|
+
// alignment. The WAL checkpoint before export is the primary
|
|
707
|
+
// consistency mechanism for the database.
|
|
708
|
+
let bytesWritten = 0;
|
|
709
|
+
if (file.size > 0) {
|
|
710
|
+
try {
|
|
711
|
+
const stream = createReadStream(file.diskPath, {
|
|
712
|
+
start: 0,
|
|
713
|
+
end: file.size - 1,
|
|
714
|
+
});
|
|
715
|
+
for await (const chunk of stream) {
|
|
716
|
+
const data =
|
|
717
|
+
chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
718
|
+
bytesWritten += data.length;
|
|
719
|
+
yield data;
|
|
720
|
+
}
|
|
721
|
+
} catch {
|
|
722
|
+
// File was deleted or rotated between passes — emit zeros for
|
|
723
|
+
// the full declared size so the tar structure stays valid
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// If the file shrank, pad with zeros in bounded chunks to reach
|
|
728
|
+
// the declared size without a large single allocation
|
|
729
|
+
let remaining = file.size - bytesWritten;
|
|
730
|
+
while (remaining > 0) {
|
|
731
|
+
const chunkSize = Math.min(remaining, 65536);
|
|
732
|
+
yield new Uint8Array(chunkSize);
|
|
733
|
+
remaining -= chunkSize;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
yield tarPaddingBytes(file.size);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// End-of-archive: two zero blocks
|
|
740
|
+
yield new Uint8Array(BLOCK_SIZE * 2);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ---------------------------------------------------------------------------
|
|
744
|
+
// Streaming export result type
|
|
745
|
+
// ---------------------------------------------------------------------------
|
|
746
|
+
|
|
747
|
+
export interface StreamExportVBundleResult {
|
|
748
|
+
tempPath: string;
|
|
749
|
+
size: number;
|
|
750
|
+
manifest: ManifestType;
|
|
751
|
+
cleanup: () => Promise<void>;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Build a .vbundle archive using a streaming two-pass approach that keeps
|
|
756
|
+
* peak memory usage bounded to ~1 MB regardless of workspace size.
|
|
757
|
+
*
|
|
758
|
+
* Pass 1: Walk directory metadata and compute SHA-256 checksums without
|
|
759
|
+
* loading file contents into memory (builds manifest).
|
|
760
|
+
* Pass 2: Stream tar entries through gzip into a temp file on disk.
|
|
761
|
+
*
|
|
762
|
+
* Returns a result with the temp file path, size, manifest, and a cleanup
|
|
763
|
+
* function to remove the temp file when done.
|
|
764
|
+
*/
|
|
765
|
+
export async function streamExportVBundle(
|
|
766
|
+
options: BuildExportVBundleOptions,
|
|
767
|
+
): Promise<StreamExportVBundleResult> {
|
|
768
|
+
const { source, description, checkpoint, trustPath, workspaceDir, hooksDir } =
|
|
769
|
+
options;
|
|
770
|
+
|
|
771
|
+
// Flush WAL to the main database file before reading
|
|
772
|
+
if (checkpoint) {
|
|
773
|
+
checkpoint();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const allFileMetadata: FileMetadata[] = [];
|
|
777
|
+
|
|
778
|
+
// Walk the entire workspace directory, including binary files
|
|
779
|
+
if (
|
|
780
|
+
workspaceDir &&
|
|
781
|
+
existsSync(workspaceDir) &&
|
|
782
|
+
lstatSync(workspaceDir).isDirectory()
|
|
783
|
+
) {
|
|
784
|
+
allFileMetadata.push(
|
|
785
|
+
...walkDirectoryForMetadata(workspaceDir, "workspace", {
|
|
786
|
+
includeBinary: true,
|
|
787
|
+
skipDirs: ["embedding-models", "data/qdrant", "signals", "deprecated"],
|
|
788
|
+
}),
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Include hooks directory if it exists
|
|
793
|
+
if (hooksDir && existsSync(hooksDir) && lstatSync(hooksDir).isDirectory()) {
|
|
794
|
+
allFileMetadata.push(...walkDirectoryForMetadata(hooksDir, "hooks"));
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Include trust rules if the file exists
|
|
798
|
+
if (trustPath && existsSync(trustPath)) {
|
|
799
|
+
const trustStat = lstatSync(trustPath);
|
|
800
|
+
if (trustStat.isFile()) {
|
|
801
|
+
allFileMetadata.push({
|
|
802
|
+
archivePath: "trust/trust.json",
|
|
803
|
+
diskPath: trustPath,
|
|
804
|
+
size: trustStat.size,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// ------------------------------------------------------------------
|
|
810
|
+
// Pass 1: Compute SHA-256 checksums to build the manifest
|
|
811
|
+
// ------------------------------------------------------------------
|
|
812
|
+
|
|
813
|
+
const fileEntries: ManifestFileEntryType[] = [];
|
|
814
|
+
for (const file of allFileMetadata) {
|
|
815
|
+
const sha256 = await computeFileSha256(file.diskPath, file.size);
|
|
816
|
+
fileEntries.push({
|
|
817
|
+
path: file.archivePath,
|
|
818
|
+
sha256,
|
|
819
|
+
size: file.size,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const manifestWithoutChecksum = {
|
|
824
|
+
schema_version: "1.0",
|
|
825
|
+
created_at: new Date().toISOString(),
|
|
826
|
+
source: source ?? "runtime-export",
|
|
827
|
+
description: description ?? "Runtime export bundle",
|
|
828
|
+
files: fileEntries,
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const manifestSha256 = sha256Hex(canonicalizeJson(manifestWithoutChecksum));
|
|
832
|
+
const manifest: ManifestType = {
|
|
833
|
+
...manifestWithoutChecksum,
|
|
834
|
+
manifest_sha256: manifestSha256,
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
const manifestData = new TextEncoder().encode(JSON.stringify(manifest));
|
|
838
|
+
|
|
839
|
+
// ------------------------------------------------------------------
|
|
840
|
+
// Pass 2: Stream tar through gzip into a temp file
|
|
841
|
+
// ------------------------------------------------------------------
|
|
842
|
+
|
|
843
|
+
const tempPath = join(tmpdir(), `vbundle-export-${randomUUID()}.tmp`);
|
|
844
|
+
|
|
845
|
+
const tarGenerator = generateTarStream(manifestData, allFileMetadata);
|
|
846
|
+
const tarReadable = Readable.from(tarGenerator);
|
|
847
|
+
const gzipStream = createGzip();
|
|
848
|
+
const writeStream = createWriteStream(tempPath, { mode: 0o600 });
|
|
849
|
+
|
|
850
|
+
try {
|
|
851
|
+
await pipeline(tarReadable, gzipStream, writeStream);
|
|
852
|
+
} catch (error) {
|
|
853
|
+
await unlink(tempPath).catch(() => {});
|
|
854
|
+
throw error;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const tempStat = await stat(tempPath);
|
|
858
|
+
|
|
859
|
+
const cleanup = async () => {
|
|
860
|
+
try {
|
|
861
|
+
await unlink(tempPath);
|
|
862
|
+
} catch {
|
|
863
|
+
// Ignore errors during cleanup
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
|
|
867
|
+
return { tempPath, size: tempStat.size, manifest, cleanup };
|
|
868
|
+
}
|
|
@@ -163,15 +163,16 @@ export function commitImport(options: ImportCommitOptions): ImportCommitResult {
|
|
|
163
163
|
entryMap = validation.entries;
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
// Directories to preserve when clearing the workspace
|
|
166
|
+
// Directories to preserve when clearing the workspace.
|
|
167
167
|
const WORKSPACE_SKIP_DIRS = new Set(["embedding-models", "deprecated"]);
|
|
168
|
-
// data/qdrant
|
|
169
|
-
const DATA_SKIP_DIRS = new Set(["qdrant"]);
|
|
168
|
+
// data/qdrant and data/db are nested — we skip them inside "data/"
|
|
169
|
+
const DATA_SKIP_DIRS = new Set(["qdrant", "db"]);
|
|
170
170
|
|
|
171
171
|
// Step 1b: Clear the workspace directory before restore if the bundle
|
|
172
172
|
// contains new-format workspace/ entries. This ensures an exact-match
|
|
173
|
-
// restore with no stale files left behind. Skips embedding-models
|
|
174
|
-
// data/qdrant/ (large, regenerable)
|
|
173
|
+
// restore with no stale files left behind. Skips embedding-models/,
|
|
174
|
+
// data/qdrant/ (large, regenerable), and data/db/ (critical — prevents
|
|
175
|
+
// data loss if the import fails partway or the archive omits the DB).
|
|
175
176
|
//
|
|
176
177
|
// Only new-format bundles (workspace/ prefix) trigger clearing. Old-format
|
|
177
178
|
// bundles (skills/, hooks/, data/db/*, config/*) wrote specific files
|
|
@@ -195,7 +196,8 @@ export function commitImport(options: ImportCommitOptions): ImportCommitResult {
|
|
|
195
196
|
|
|
196
197
|
const entryPath = join(workspaceDir, entry.name);
|
|
197
198
|
if (entry.name === "data" && entry.isDirectory()) {
|
|
198
|
-
// Inside data/, preserve qdrant/
|
|
199
|
+
// Inside data/, preserve qdrant/ (large, regenerable) and db/
|
|
200
|
+
// (critical user data) but clear everything else
|
|
199
201
|
const dataEntries = readdirSync(entryPath, { withFileTypes: true });
|
|
200
202
|
for (const dataEntry of dataEntries) {
|
|
201
203
|
if (DATA_SKIP_DIRS.has(dataEntry.name)) continue;
|