@pellux/goodvibes-agent 1.1.6 → 1.2.0
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/CHANGELOG.md +176 -0
- package/README.md +46 -30
- package/dist/package/{ast-grep-napi.linux-x64-gnu-mkk8xwww.node → ast-grep-napi.linux-x64-gnu-swtppvy9.node} +0 -0
- package/dist/package/{ast-grep-napi.linux-x64-musl-ryqtgdv6.node → ast-grep-napi.linux-x64-musl-ttfcdtap.node} +0 -0
- package/dist/package/main.js +132662 -91399
- package/docs/README.md +14 -7
- package/docs/channels-remote-and-api.md +8 -5
- package/docs/connected-host.md +14 -12
- package/docs/getting-started.md +56 -30
- package/docs/knowledge-artifacts-and-multimodal.md +7 -5
- package/docs/project-planning.md +2 -2
- package/docs/providers-and-routing.md +11 -3
- package/docs/tools-and-commands.md +114 -45
- package/docs/voice-and-live-tts.md +3 -1
- package/package.json +3 -2
- package/release/release-notes.md +130 -5
- package/release/release-readiness.json +120 -90
- package/src/agent/assistant-cockpit.ts +247 -0
- package/src/agent/autonomy-schedule-format.ts +96 -0
- package/src/agent/autonomy-schedule.ts +514 -0
- package/src/agent/channel-delivery-receipts.ts +292 -0
- package/src/agent/competitive-feature-inventory.ts +296 -0
- package/src/agent/document-registry-types.ts +106 -0
- package/src/agent/document-registry.ts +734 -0
- package/src/agent/harness-control.ts +2 -2
- package/src/agent/memory-prompt.ts +2 -2
- package/src/agent/operator-actions.ts +50 -6
- package/src/agent/persona-registry.ts +8 -0
- package/src/agent/project-context-files.ts +273 -0
- package/src/agent/prompt-context-receipts.ts +502 -0
- package/src/agent/reminder-schedule-format.ts +16 -2
- package/src/agent/research-run-registry.ts +582 -0
- package/src/agent/research-source-registry.ts +441 -0
- package/src/agent/routine-registry.ts +19 -3
- package/src/agent/routine-schedule-format.ts +27 -2
- package/src/agent/runtime-profile-starters.ts +7 -0
- package/src/agent/runtime-profile.ts +106 -4
- package/src/agent/schedule-edit-format.ts +129 -0
- package/src/agent/schedule-edit.ts +496 -0
- package/src/agent/schedule-next-routes.ts +42 -0
- package/src/agent/setup-wizard-checkpoint.ts +140 -0
- package/src/agent/setup-wizard.ts +447 -0
- package/src/agent/skill-registry-types.ts +102 -0
- package/src/agent/skill-registry.ts +81 -107
- package/src/agent/vibe-confirmation-routes.ts +62 -0
- package/src/agent/vibe-file.ts +285 -0
- package/src/cli/agent-knowledge-command.ts +45 -2
- package/src/cli/help.ts +3 -0
- package/src/cli/profiles-command.ts +25 -11
- package/src/config/agent-settings-policy.ts +1 -18
- package/src/input/agent-workspace-activation.ts +53 -0
- package/src/input/agent-workspace-artifact-browser-editor.ts +494 -0
- package/src/input/agent-workspace-artifact-metadata.ts +17 -0
- package/src/input/agent-workspace-basic-command-editors.ts +5 -1
- package/src/input/agent-workspace-categories.ts +153 -155
- package/src/input/agent-workspace-channel-triage.ts +481 -0
- package/src/input/agent-workspace-channels.ts +263 -0
- package/src/input/agent-workspace-command-editor.ts +152 -0
- package/src/input/agent-workspace-context-snapshot.ts +349 -0
- package/src/input/agent-workspace-delegation-editor-submission.ts +8 -0
- package/src/input/agent-workspace-document-editor.ts +502 -0
- package/src/input/agent-workspace-document-ops-editor.ts +523 -0
- package/src/input/agent-workspace-host-category.ts +1 -1
- package/src/input/agent-workspace-local-library-snapshot.ts +167 -0
- package/src/input/agent-workspace-model-compare-editor.ts +376 -0
- package/src/input/agent-workspace-model-compare-prompt-submission.ts +552 -0
- package/src/input/agent-workspace-model-compare-types.ts +82 -0
- package/src/input/agent-workspace-model-compare-utils.ts +35 -0
- package/src/input/agent-workspace-onboarding-categories.ts +141 -0
- package/src/input/agent-workspace-operations-command-editor-submission.ts +53 -0
- package/src/input/agent-workspace-operations-command-editors.ts +22 -0
- package/src/input/agent-workspace-research-report-editor.ts +212 -0
- package/src/input/agent-workspace-research-run-editor.ts +144 -0
- package/src/input/agent-workspace-research-source-editor.ts +167 -0
- package/src/input/agent-workspace-review-packet-snapshot.ts +593 -0
- package/src/input/agent-workspace-review-packet-utils.ts +265 -0
- package/src/input/agent-workspace-settings.ts +262 -30
- package/src/input/agent-workspace-setup-checkpoint-action.ts +127 -0
- package/src/input/agent-workspace-setup-snapshot.ts +198 -0
- package/src/input/agent-workspace-setup.ts +34 -1
- package/src/input/agent-workspace-snapshot.ts +162 -147
- package/src/input/agent-workspace-types.ts +287 -2
- package/src/input/agent-workspace.ts +18 -0
- package/src/input/command-registry.ts +9 -2
- package/src/input/commands/agent-runtime-profile-runtime.ts +28 -13
- package/src/input/commands/channels-runtime.ts +102 -114
- package/src/input/commands/delegation-runtime.ts +87 -24
- package/src/input/commands/knowledge-browser-flags.ts +12 -0
- package/src/input/commands/knowledge.ts +28 -14
- package/src/input/commands/operator-actions-runtime.ts +6 -3
- package/src/input/commands/schedule-runtime.ts +71 -14
- package/src/input/commands/session-content.ts +2 -2
- package/src/input/commands/shell-core.ts +2 -0
- package/src/input/commands/vibe-runtime.ts +140 -0
- package/src/input/commands.ts +2 -0
- package/src/input/connected-host-routes.ts +123 -0
- package/src/input/settings-modal-types.ts +14 -0
- package/src/main.ts +15 -15
- package/src/panels/qr-panel.ts +2 -2
- package/src/renderer/agent-workspace-context-lines.ts +686 -0
- package/src/renderer/agent-workspace.ts +30 -419
- package/src/renderer/settings-modal-helpers.ts +12 -0
- package/src/renderer/settings-modal.ts +12 -0
- package/src/runtime/bootstrap-command-context.ts +11 -1
- package/src/runtime/bootstrap-command-parts.ts +14 -3
- package/src/runtime/bootstrap-core.ts +43 -1
- package/src/runtime/bootstrap-shell.ts +6 -0
- package/src/runtime/bootstrap.ts +113 -30
- package/src/runtime/connected-host-auth.ts +3 -2
- package/src/runtime/execution-ledger.ts +231 -0
- package/src/runtime/services.ts +88 -10
- package/src/runtime/tool-permission-safety.ts +287 -0
- package/src/shell/session-continuity-hints.ts +39 -0
- package/src/tools/agent-artifacts-tool.ts +606 -0
- package/src/tools/agent-audit-tool.ts +155 -0
- package/src/tools/agent-autonomy-schedule-tool.ts +238 -0
- package/src/tools/agent-autonomy-tool.ts +140 -0
- package/src/tools/agent-channel-send-tool.ts +24 -2
- package/src/tools/agent-channels-tool.ts +140 -0
- package/src/tools/agent-computer-tool.ts +168 -0
- package/src/tools/agent-context-policy.ts +267 -10
- package/src/tools/agent-context-tool.ts +143 -0
- package/src/tools/agent-delegation-tool.ts +128 -0
- package/src/tools/agent-device-tool.ts +240 -0
- package/src/tools/agent-documents-tool.ts +684 -0
- package/src/tools/agent-execution-tool.ts +230 -0
- package/src/tools/agent-harness-agent-orchestration.ts +703 -0
- package/src/tools/agent-harness-autonomy-intake.ts +785 -0
- package/src/tools/agent-harness-autonomy-live-records.ts +588 -0
- package/src/tools/agent-harness-autonomy-queue-types.ts +88 -0
- package/src/tools/agent-harness-autonomy-queue.ts +560 -0
- package/src/tools/agent-harness-background-processes-types.ts +35 -0
- package/src/tools/agent-harness-background-processes.ts +794 -0
- package/src/tools/agent-harness-browser-cockpit-route.ts +219 -0
- package/src/tools/agent-harness-browser-control.ts +338 -0
- package/src/tools/agent-harness-channel-metadata.ts +168 -10
- package/src/tools/agent-harness-cli-command-policy.ts +110 -0
- package/src/tools/agent-harness-command-catalog.ts +4 -2
- package/src/tools/agent-harness-command-runner.ts +42 -0
- package/src/tools/agent-harness-connected-host-status.ts +7 -4
- package/src/tools/agent-harness-delegation-posture.ts +234 -19
- package/src/tools/agent-harness-document-ops-reviewer-readiness.ts +234 -0
- package/src/tools/agent-harness-document-ops-types.ts +72 -0
- package/src/tools/agent-harness-document-ops.ts +671 -0
- package/src/tools/agent-harness-execution-history.ts +489 -0
- package/src/tools/agent-harness-execution-posture.ts +382 -0
- package/src/tools/agent-harness-file-recovery.ts +135 -0
- package/src/tools/agent-harness-keybinding-metadata.ts +16 -12
- package/src/tools/agent-harness-learning-curator-common.ts +102 -0
- package/src/tools/agent-harness-learning-curator-consolidation.ts +295 -0
- package/src/tools/agent-harness-learning-curator-proposals.ts +606 -0
- package/src/tools/agent-harness-learning-curator-types.ts +151 -0
- package/src/tools/agent-harness-learning-curator.ts +417 -0
- package/src/tools/agent-harness-local-model-benchmarks.ts +199 -0
- package/src/tools/agent-harness-local-model-cookbook.ts +238 -0
- package/src/tools/agent-harness-local-model-endpoints.ts +673 -0
- package/src/tools/agent-harness-media-posture.ts +208 -2
- package/src/tools/agent-harness-memory-posture.ts +556 -0
- package/src/tools/agent-harness-metadata.ts +100 -168
- package/src/tools/agent-harness-mode-catalog.ts +84 -33
- package/src/tools/agent-harness-model-catalog.ts +162 -0
- package/src/tools/agent-harness-model-provider-health.ts +186 -0
- package/src/tools/agent-harness-model-readiness.ts +406 -0
- package/src/tools/agent-harness-model-routing-types.ts +266 -0
- package/src/tools/agent-harness-model-routing-utils.ts +30 -0
- package/src/tools/agent-harness-model-routing.ts +137 -190
- package/src/tools/agent-harness-operator-methods.ts +115 -133
- package/src/tools/agent-harness-pairing-posture.ts +431 -18
- package/src/tools/agent-harness-personal-ops-discovery.ts +533 -0
- package/src/tools/agent-harness-personal-ops-intake.ts +385 -0
- package/src/tools/agent-harness-personal-ops-lanes.ts +204 -0
- package/src/tools/agent-harness-personal-ops-records.ts +732 -0
- package/src/tools/agent-harness-personal-ops-runner.ts +637 -0
- package/src/tools/agent-harness-personal-ops-types.ts +222 -0
- package/src/tools/agent-harness-personal-ops.ts +606 -0
- package/src/tools/agent-harness-project-context.ts +144 -0
- package/src/tools/agent-harness-prompt-context.ts +589 -0
- package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
- package/src/tools/agent-harness-release-evidence.ts +9 -7
- package/src/tools/agent-harness-release-readiness.ts +9 -7
- package/src/tools/agent-harness-research-briefing.ts +427 -0
- package/src/tools/agent-harness-research-queue.ts +250 -0
- package/src/tools/agent-harness-research-runs.ts +295 -0
- package/src/tools/agent-harness-research-workflow.ts +322 -0
- package/src/tools/agent-harness-security-posture.ts +2 -2
- package/src/tools/agent-harness-service-posture.ts +9 -9
- package/src/tools/agent-harness-session-metadata.ts +3 -3
- package/src/tools/agent-harness-setup-connected-host.ts +501 -0
- package/src/tools/agent-harness-setup-describe.ts +557 -0
- package/src/tools/agent-harness-setup-handoffs.ts +502 -0
- package/src/tools/agent-harness-setup-model-helpers.ts +101 -0
- package/src/tools/agent-harness-setup-plan.ts +253 -0
- package/src/tools/agent-harness-setup-posture-types.ts +218 -0
- package/src/tools/agent-harness-setup-posture-utils.ts +173 -0
- package/src/tools/agent-harness-setup-posture.ts +449 -221
- package/src/tools/agent-harness-setup-smoke.ts +508 -0
- package/src/tools/agent-harness-sudo-posture.ts +114 -0
- package/src/tools/agent-harness-tool-schema.ts +120 -7
- package/src/tools/agent-harness-tool-types.ts +78 -0
- package/src/tools/agent-harness-tool-utils.ts +43 -0
- package/src/tools/agent-harness-tool.ts +311 -370
- package/src/tools/agent-harness-ui-surface-metadata.ts +76 -69
- package/src/tools/agent-harness-ui-surface-types.ts +46 -0
- package/src/tools/agent-harness-vibe-health.ts +105 -0
- package/src/tools/agent-harness-workspace-action-runner.ts +149 -0
- package/src/tools/agent-harness-workspace-actions.ts +114 -8
- package/src/tools/agent-harness-workspace-editor-execution.ts +214 -0
- package/src/tools/agent-harness-workspace-editor-runner.ts +755 -0
- package/src/tools/agent-host-tool.ts +159 -0
- package/src/tools/agent-knowledge-ingest-tool.ts +34 -1
- package/src/tools/agent-learning-consolidation-core.ts +327 -0
- package/src/tools/agent-learning-consolidation-tool.ts +758 -0
- package/src/tools/agent-memory-tool.ts +214 -0
- package/src/tools/agent-model-compare-export.ts +315 -0
- package/src/tools/agent-model-compare-handoff.ts +592 -0
- package/src/tools/agent-model-compare-judgment.ts +633 -0
- package/src/tools/agent-model-compare-run.ts +722 -0
- package/src/tools/agent-model-compare-tool.ts +698 -0
- package/src/tools/agent-model-compare-types.ts +191 -0
- package/src/tools/agent-model-compare-utils.ts +93 -0
- package/src/tools/agent-models-tool.ts +208 -0
- package/src/tools/agent-operator-action-tool.ts +1 -1
- package/src/tools/agent-operator-method-tool.ts +643 -0
- package/src/tools/agent-personal-ops-tool.ts +197 -0
- package/src/tools/agent-policy-explanation.ts +415 -0
- package/src/tools/agent-research-report-tool.ts +608 -0
- package/src/tools/agent-research-runs-tool.ts +434 -0
- package/src/tools/agent-research-sources-tool.ts +443 -0
- package/src/tools/agent-research-tool.ts +687 -0
- package/src/tools/agent-review-packet-presets-core.ts +757 -0
- package/src/tools/agent-review-packet-presets-tool.ts +466 -0
- package/src/tools/agent-review-packet-share-tool.ts +305 -0
- package/src/tools/agent-route-planner-candidates-setup.ts +411 -0
- package/src/tools/agent-route-planner-candidates-surfaces.ts +432 -0
- package/src/tools/agent-route-planner-candidates-work.ts +251 -0
- package/src/tools/agent-route-planner-helpers.ts +667 -0
- package/src/tools/agent-route-planner.ts +185 -0
- package/src/tools/agent-route-tool.ts +105 -0
- package/src/tools/agent-schedule-edit-tool.ts +210 -0
- package/src/tools/agent-schedule-tool.ts +282 -0
- package/src/tools/agent-security-tool.ts +145 -0
- package/src/tools/agent-sessions-tool.ts +122 -0
- package/src/tools/agent-settings-import-tool.ts +104 -0
- package/src/tools/agent-settings-tool.ts +176 -0
- package/src/tools/agent-setup-tool.ts +226 -0
- package/src/tools/agent-support-tool.ts +122 -0
- package/src/tools/agent-terminal-process-tools.ts +167 -0
- package/src/tools/agent-tool-policy-guard-types.ts +77 -0
- package/src/tools/agent-tool-policy-guard.ts +110 -89
- package/src/tools/agent-vibe-tool.ts +297 -0
- package/src/tools/agent-work-plan-tool.ts +265 -6
- package/src/tools/agent-workspace-tool.ts +305 -0
- package/src/tools/artifact-archive.ts +169 -0
- package/src/tools/tool-definition-compaction.ts +36 -0
- package/src/tools/tool-execution-safety.ts +41 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
|
+
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
|
+
import { assertNoSecretLikeText } from './persona-registry.ts';
|
|
6
|
+
import type {
|
|
7
|
+
AgentDocumentAttachment,
|
|
8
|
+
AgentDocumentAttachmentInput,
|
|
9
|
+
AgentDocumentComment,
|
|
10
|
+
AgentDocumentCommentInput,
|
|
11
|
+
AgentDocumentCommentStatus,
|
|
12
|
+
AgentDocumentCreateInput,
|
|
13
|
+
AgentDocumentRecord,
|
|
14
|
+
AgentDocumentSnapshot,
|
|
15
|
+
AgentDocumentStatus,
|
|
16
|
+
AgentDocumentSuggestion,
|
|
17
|
+
AgentDocumentSuggestionInput,
|
|
18
|
+
AgentDocumentSuggestionStatus,
|
|
19
|
+
AgentDocumentUpdateInput,
|
|
20
|
+
AgentDocumentVersion,
|
|
21
|
+
} from './document-registry-types.ts';
|
|
22
|
+
export type {
|
|
23
|
+
AgentDocumentAttachment,
|
|
24
|
+
AgentDocumentAttachmentInput,
|
|
25
|
+
AgentDocumentComment,
|
|
26
|
+
AgentDocumentCommentInput,
|
|
27
|
+
AgentDocumentCommentStatus,
|
|
28
|
+
AgentDocumentCreateInput,
|
|
29
|
+
AgentDocumentRecord,
|
|
30
|
+
AgentDocumentSnapshot,
|
|
31
|
+
AgentDocumentStatus,
|
|
32
|
+
AgentDocumentSuggestion,
|
|
33
|
+
AgentDocumentSuggestionInput,
|
|
34
|
+
AgentDocumentSuggestionStatus,
|
|
35
|
+
AgentDocumentUpdateInput,
|
|
36
|
+
AgentDocumentVersion,
|
|
37
|
+
} from './document-registry-types.ts';
|
|
38
|
+
|
|
39
|
+
interface DocumentStoreFile {
|
|
40
|
+
readonly version: 1;
|
|
41
|
+
readonly documents: readonly AgentDocumentRecord[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type AgentDocumentStorePaths = Pick<ShellPathService, 'resolveProjectPath'>;
|
|
45
|
+
|
|
46
|
+
const STORE_VERSION = 1;
|
|
47
|
+
const MAX_VERSIONS = 50;
|
|
48
|
+
|
|
49
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
50
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readString(value: unknown, fallback = ''): string {
|
|
54
|
+
return typeof value === 'string' ? value : fallback;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readStringArray(value: unknown): string[] {
|
|
58
|
+
if (!Array.isArray(value)) return [];
|
|
59
|
+
return value.filter((entry): entry is string => typeof entry === 'string').map((entry) => entry.trim()).filter(Boolean);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeTitle(value: string): string {
|
|
63
|
+
return value.trim().replace(/\s+/g, ' ');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeList(values: readonly string[] | undefined): string[] {
|
|
67
|
+
const seen = new Set<string>();
|
|
68
|
+
const result: string[] = [];
|
|
69
|
+
for (const value of values ?? []) {
|
|
70
|
+
const trimmed = value.trim();
|
|
71
|
+
if (!trimmed) continue;
|
|
72
|
+
const key = trimmed.toLowerCase();
|
|
73
|
+
if (seen.has(key)) continue;
|
|
74
|
+
seen.add(key);
|
|
75
|
+
result.push(trimmed);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function slugify(value: string): string {
|
|
81
|
+
const slug = value.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
82
|
+
return slug || 'document';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function nowIso(): string {
|
|
86
|
+
return new Date().toISOString();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function documentStatus(value: unknown): AgentDocumentStatus {
|
|
90
|
+
if (value === 'reviewed' || value === 'archived') return value;
|
|
91
|
+
return 'draft';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function commentStatus(value: unknown): AgentDocumentCommentStatus {
|
|
95
|
+
return value === 'resolved' ? 'resolved' : 'open';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function suggestionStatus(value: unknown): AgentDocumentSuggestionStatus {
|
|
99
|
+
if (value === 'accepted' || value === 'rejected') return value;
|
|
100
|
+
return 'proposed';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function assertDocumentContentSafe(fields: readonly string[]): void {
|
|
104
|
+
try {
|
|
105
|
+
assertNoSecretLikeText(fields);
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error('Documents cannot store secret-looking values. Store a secret reference or remove the sensitive text.');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseVersion(value: unknown): AgentDocumentVersion | null {
|
|
112
|
+
if (!isRecord(value)) return null;
|
|
113
|
+
const id = readString(value.id).trim();
|
|
114
|
+
const title = normalizeTitle(readString(value.title));
|
|
115
|
+
const body = readString(value.body).trim();
|
|
116
|
+
const summary = readString(value.summary).trim();
|
|
117
|
+
if (!id || !title || !body) return null;
|
|
118
|
+
return {
|
|
119
|
+
id,
|
|
120
|
+
title,
|
|
121
|
+
body,
|
|
122
|
+
summary,
|
|
123
|
+
createdAt: readString(value.createdAt, nowIso()),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseComment(value: unknown): AgentDocumentComment | null {
|
|
128
|
+
if (!isRecord(value)) return null;
|
|
129
|
+
const id = readString(value.id).trim();
|
|
130
|
+
const body = readString(value.body).trim();
|
|
131
|
+
if (!id || !body) return null;
|
|
132
|
+
const createdAt = readString(value.createdAt, nowIso());
|
|
133
|
+
const resolvedAt = readString(value.resolvedAt).trim();
|
|
134
|
+
return {
|
|
135
|
+
id,
|
|
136
|
+
body,
|
|
137
|
+
status: commentStatus(value.status),
|
|
138
|
+
createdAt,
|
|
139
|
+
updatedAt: readString(value.updatedAt, createdAt),
|
|
140
|
+
resolvedAt: resolvedAt || undefined,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseSuggestion(value: unknown): AgentDocumentSuggestion | null {
|
|
145
|
+
if (!isRecord(value)) return null;
|
|
146
|
+
const id = readString(value.id).trim();
|
|
147
|
+
const title = normalizeTitle(readString(value.title));
|
|
148
|
+
const body = readString(value.body).trim();
|
|
149
|
+
const summary = readString(value.summary).trim();
|
|
150
|
+
if (!id || !title || !body || !summary) return null;
|
|
151
|
+
const createdAt = readString(value.createdAt, nowIso());
|
|
152
|
+
const resolvedAt = readString(value.resolvedAt).trim();
|
|
153
|
+
return {
|
|
154
|
+
id,
|
|
155
|
+
title,
|
|
156
|
+
body,
|
|
157
|
+
tags: readStringArray(value.tags),
|
|
158
|
+
...(value.documentStatus === undefined ? {} : { documentStatus: documentStatus(value.documentStatus) }),
|
|
159
|
+
summary,
|
|
160
|
+
rationale: readString(value.rationale).trim(),
|
|
161
|
+
status: suggestionStatus(value.status),
|
|
162
|
+
createdAt,
|
|
163
|
+
updatedAt: readString(value.updatedAt, createdAt),
|
|
164
|
+
resolvedAt: resolvedAt || undefined,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function parseAttachment(value: unknown): AgentDocumentAttachment | null {
|
|
169
|
+
if (!isRecord(value)) return null;
|
|
170
|
+
const id = readString(value.id).trim();
|
|
171
|
+
const artifactId = readString(value.artifactId).trim();
|
|
172
|
+
const label = readString(value.label).trim();
|
|
173
|
+
if (!id || !artifactId || !label) return null;
|
|
174
|
+
const createdAt = readString(value.createdAt, nowIso());
|
|
175
|
+
const sizeBytes = typeof value.sizeBytes === 'number' && Number.isFinite(value.sizeBytes)
|
|
176
|
+
? Math.max(0, Math.trunc(value.sizeBytes))
|
|
177
|
+
: undefined;
|
|
178
|
+
const note = readString(value.note).trim();
|
|
179
|
+
const filename = readString(value.filename).trim();
|
|
180
|
+
const mimeType = readString(value.mimeType).trim();
|
|
181
|
+
const kind = readString(value.kind).trim();
|
|
182
|
+
return {
|
|
183
|
+
id,
|
|
184
|
+
artifactId,
|
|
185
|
+
label,
|
|
186
|
+
...(note ? { note } : {}),
|
|
187
|
+
...(filename ? { filename } : {}),
|
|
188
|
+
...(mimeType ? { mimeType } : {}),
|
|
189
|
+
...(kind ? { kind } : {}),
|
|
190
|
+
...(sizeBytes === undefined ? {} : { sizeBytes }),
|
|
191
|
+
createdAt,
|
|
192
|
+
updatedAt: readString(value.updatedAt, createdAt),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function parseDocument(value: unknown): AgentDocumentRecord | null {
|
|
197
|
+
if (!isRecord(value)) return null;
|
|
198
|
+
const id = readString(value.id).trim();
|
|
199
|
+
const title = normalizeTitle(readString(value.title));
|
|
200
|
+
const body = readString(value.body).trim();
|
|
201
|
+
if (!id || !title || !body) return null;
|
|
202
|
+
const createdAt = readString(value.createdAt, nowIso());
|
|
203
|
+
const versions = Array.isArray(value.versions)
|
|
204
|
+
? value.versions.map(parseVersion).filter((entry): entry is AgentDocumentVersion => entry !== null)
|
|
205
|
+
: [];
|
|
206
|
+
const comments = Array.isArray(value.comments)
|
|
207
|
+
? value.comments.map(parseComment).filter((entry): entry is AgentDocumentComment => entry !== null)
|
|
208
|
+
: [];
|
|
209
|
+
const suggestions = Array.isArray(value.suggestions)
|
|
210
|
+
? value.suggestions.map(parseSuggestion).filter((entry): entry is AgentDocumentSuggestion => entry !== null)
|
|
211
|
+
: [];
|
|
212
|
+
const attachments = Array.isArray(value.attachments)
|
|
213
|
+
? value.attachments.map(parseAttachment).filter((entry): entry is AgentDocumentAttachment => entry !== null)
|
|
214
|
+
: [];
|
|
215
|
+
const lastArtifactId = readString(value.lastArtifactId).trim();
|
|
216
|
+
return {
|
|
217
|
+
id,
|
|
218
|
+
title,
|
|
219
|
+
body,
|
|
220
|
+
tags: readStringArray(value.tags),
|
|
221
|
+
status: documentStatus(value.status),
|
|
222
|
+
createdAt,
|
|
223
|
+
updatedAt: readString(value.updatedAt, createdAt),
|
|
224
|
+
versions,
|
|
225
|
+
comments,
|
|
226
|
+
suggestions,
|
|
227
|
+
attachments,
|
|
228
|
+
lastArtifactId: lastArtifactId || undefined,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseStore(raw: string): DocumentStoreFile {
|
|
233
|
+
const parsed: unknown = JSON.parse(raw);
|
|
234
|
+
if (!isRecord(parsed)) return { version: STORE_VERSION, documents: [] };
|
|
235
|
+
return {
|
|
236
|
+
version: STORE_VERSION,
|
|
237
|
+
documents: Array.isArray(parsed.documents)
|
|
238
|
+
? parsed.documents.map(parseDocument).filter((entry): entry is AgentDocumentRecord => entry !== null)
|
|
239
|
+
: [],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatStore(store: DocumentStoreFile): string {
|
|
244
|
+
return `${JSON.stringify(store, null, 2)}\n`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function compactExportLine(value: string): string {
|
|
248
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function renderCommentSummary(comment: AgentDocumentComment): string {
|
|
252
|
+
const resolved = comment.resolvedAt ? ` resolved ${comment.resolvedAt}` : '';
|
|
253
|
+
return `- ${comment.id} [${comment.status}] ${compactExportLine(comment.body)} (created ${comment.createdAt}${resolved})`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function renderSuggestionSummary(suggestion: AgentDocumentSuggestion): string {
|
|
257
|
+
const parts = [
|
|
258
|
+
`- ${suggestion.id} [${suggestion.status}] ${compactExportLine(suggestion.summary)}`,
|
|
259
|
+
suggestion.rationale ? `Rationale: ${compactExportLine(suggestion.rationale)}` : '',
|
|
260
|
+
`Title: ${suggestion.title}`,
|
|
261
|
+
suggestion.tags.length > 0 ? `Tags: ${suggestion.tags.join(', ')}` : '',
|
|
262
|
+
suggestion.documentStatus ? `Status after accept: ${suggestion.documentStatus}` : '',
|
|
263
|
+
suggestion.resolvedAt ? `Resolved: ${suggestion.resolvedAt}` : '',
|
|
264
|
+
].filter(Boolean);
|
|
265
|
+
return parts.join(' | ');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function documentStorePath(shellPaths: AgentDocumentStorePaths): string {
|
|
269
|
+
return shellPaths.resolveProjectPath(GOODVIBES_AGENT_SURFACE_ROOT, 'documents', 'documents.json');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function renderAgentDocumentMarkdown(document: AgentDocumentRecord): string {
|
|
273
|
+
const tags = document.tags.length > 0 ? `\nTags: ${document.tags.join(', ')}` : '';
|
|
274
|
+
const openComments = document.comments.filter((comment) => comment.status === 'open').length;
|
|
275
|
+
const comments = document.comments.length > 0 ? `\nComments: ${openComments} open / ${document.comments.length} total` : '';
|
|
276
|
+
const proposedSuggestions = document.suggestions.filter((suggestion) => suggestion.status === 'proposed').length;
|
|
277
|
+
const suggestions = document.suggestions.length > 0 ? `\nSuggestions: ${proposedSuggestions} proposed / ${document.suggestions.length} total` : '';
|
|
278
|
+
const attachments = document.attachments.length > 0 ? `\nAttachments: ${document.attachments.length}` : '';
|
|
279
|
+
const lines = [
|
|
280
|
+
`# ${document.title}`,
|
|
281
|
+
'',
|
|
282
|
+
`Document ID: ${document.id}`,
|
|
283
|
+
`Version: ${document.versions.at(-1)?.id ?? 'v1'}`,
|
|
284
|
+
`Status: ${document.status}`,
|
|
285
|
+
`Updated: ${document.updatedAt}${tags}${comments}${suggestions}${attachments}`,
|
|
286
|
+
'',
|
|
287
|
+
document.body,
|
|
288
|
+
'',
|
|
289
|
+
];
|
|
290
|
+
if (document.attachments.length > 0) {
|
|
291
|
+
lines.push(
|
|
292
|
+
'## Attached Artifacts',
|
|
293
|
+
'',
|
|
294
|
+
...document.attachments.map((attachment) => {
|
|
295
|
+
const details = [
|
|
296
|
+
attachment.filename ?? '',
|
|
297
|
+
attachment.mimeType ?? '',
|
|
298
|
+
attachment.kind ?? '',
|
|
299
|
+
].filter(Boolean).join(' / ');
|
|
300
|
+
const note = attachment.note ? ` - ${attachment.note}` : '';
|
|
301
|
+
return `- ${attachment.id}: ${attachment.label} (${attachment.artifactId}${details ? `, ${details}` : ''})${note}`;
|
|
302
|
+
}),
|
|
303
|
+
'',
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
if (document.comments.length > 0) {
|
|
307
|
+
lines.push(
|
|
308
|
+
'## Review Comments',
|
|
309
|
+
'',
|
|
310
|
+
...document.comments.map(renderCommentSummary),
|
|
311
|
+
'',
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
if (document.suggestions.length > 0) {
|
|
315
|
+
lines.push(
|
|
316
|
+
'## AI Suggestions',
|
|
317
|
+
'',
|
|
318
|
+
...document.suggestions.map(renderSuggestionSummary),
|
|
319
|
+
'',
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
return lines.join('\n');
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export class AgentDocumentRegistry {
|
|
326
|
+
public constructor(private readonly storePath: string) {}
|
|
327
|
+
|
|
328
|
+
public static fromShellPaths(shellPaths: AgentDocumentStorePaths): AgentDocumentRegistry {
|
|
329
|
+
return new AgentDocumentRegistry(documentStorePath(shellPaths));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
public snapshot(): AgentDocumentSnapshot {
|
|
333
|
+
const store = this.readStore();
|
|
334
|
+
return {
|
|
335
|
+
path: this.storePath,
|
|
336
|
+
documents: [...store.documents].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt)),
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
public list(): readonly AgentDocumentRecord[] {
|
|
341
|
+
return this.snapshot().documents;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
public search(query: string): readonly AgentDocumentRecord[] {
|
|
345
|
+
const normalized = query.trim().toLowerCase();
|
|
346
|
+
if (!normalized) return this.list();
|
|
347
|
+
return this.list().filter((document) => [
|
|
348
|
+
document.id,
|
|
349
|
+
document.title,
|
|
350
|
+
document.body,
|
|
351
|
+
document.status,
|
|
352
|
+
document.lastArtifactId ?? '',
|
|
353
|
+
...document.tags,
|
|
354
|
+
...document.attachments.map((attachment) => [
|
|
355
|
+
attachment.id,
|
|
356
|
+
attachment.artifactId,
|
|
357
|
+
attachment.label,
|
|
358
|
+
attachment.note ?? '',
|
|
359
|
+
attachment.filename ?? '',
|
|
360
|
+
attachment.mimeType ?? '',
|
|
361
|
+
attachment.kind ?? '',
|
|
362
|
+
].join(' ')),
|
|
363
|
+
...document.comments.map((comment) => `${comment.id} ${comment.status} ${comment.body}`),
|
|
364
|
+
...document.suggestions.map((suggestion) => `${suggestion.id} ${suggestion.status} ${suggestion.summary} ${suggestion.rationale} ${suggestion.body}`),
|
|
365
|
+
].some((field) => field.toLowerCase().includes(normalized)));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
public get(idOrTitle: string): AgentDocumentRecord | null {
|
|
369
|
+
const lookup = idOrTitle.trim().toLowerCase();
|
|
370
|
+
if (!lookup) return null;
|
|
371
|
+
return this.list().find((document) => (
|
|
372
|
+
document.id.toLowerCase() === lookup
|
|
373
|
+
|| document.title.toLowerCase() === lookup
|
|
374
|
+
)) ?? null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
public create(input: AgentDocumentCreateInput): AgentDocumentRecord {
|
|
378
|
+
const store = this.readStore();
|
|
379
|
+
const title = normalizeTitle(input.title);
|
|
380
|
+
const body = input.body.trim();
|
|
381
|
+
this.validateRequired(title, body);
|
|
382
|
+
const tags = normalizeList(input.tags);
|
|
383
|
+
const summary = input.summary?.trim() || 'Initial draft.';
|
|
384
|
+
assertDocumentContentSafe([title, body, summary, ...tags]);
|
|
385
|
+
const duplicate = store.documents.find((document) => document.title.toLowerCase() === title.toLowerCase());
|
|
386
|
+
if (duplicate) throw new Error(`Document already exists ${duplicate.id}`);
|
|
387
|
+
const timestamp = nowIso();
|
|
388
|
+
const version: AgentDocumentVersion = {
|
|
389
|
+
id: 'v1',
|
|
390
|
+
title,
|
|
391
|
+
body,
|
|
392
|
+
summary,
|
|
393
|
+
createdAt: timestamp,
|
|
394
|
+
};
|
|
395
|
+
const document: AgentDocumentRecord = {
|
|
396
|
+
id: this.nextId(title, store.documents),
|
|
397
|
+
title,
|
|
398
|
+
body,
|
|
399
|
+
tags,
|
|
400
|
+
status: 'draft',
|
|
401
|
+
createdAt: timestamp,
|
|
402
|
+
updatedAt: timestamp,
|
|
403
|
+
versions: [version],
|
|
404
|
+
comments: [],
|
|
405
|
+
suggestions: [],
|
|
406
|
+
attachments: [],
|
|
407
|
+
};
|
|
408
|
+
this.writeStore({ ...store, documents: [...store.documents, document] });
|
|
409
|
+
return document;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
public update(idOrTitle: string, input: AgentDocumentUpdateInput): AgentDocumentRecord {
|
|
413
|
+
const store = this.readStore();
|
|
414
|
+
const existing = this.findInStore(store, idOrTitle);
|
|
415
|
+
if (!existing) throw new Error(`Unknown document ${idOrTitle}`);
|
|
416
|
+
const title = input.title === undefined ? existing.title : normalizeTitle(input.title);
|
|
417
|
+
const body = input.body === undefined ? existing.body : input.body.trim();
|
|
418
|
+
this.validateRequired(title, body);
|
|
419
|
+
const tags = input.tags === undefined ? existing.tags : normalizeList(input.tags);
|
|
420
|
+
const summary = input.summary?.trim() || 'Updated draft.';
|
|
421
|
+
assertDocumentContentSafe([title, body, summary, ...tags]);
|
|
422
|
+
const duplicate = store.documents.find((document) => document.id !== existing.id && document.title.toLowerCase() === title.toLowerCase());
|
|
423
|
+
if (duplicate) throw new Error(`Document already exists ${duplicate.id}`);
|
|
424
|
+
const timestamp = nowIso();
|
|
425
|
+
const version: AgentDocumentVersion = {
|
|
426
|
+
id: `v${existing.versions.length + 1}`,
|
|
427
|
+
title,
|
|
428
|
+
body,
|
|
429
|
+
summary,
|
|
430
|
+
createdAt: timestamp,
|
|
431
|
+
};
|
|
432
|
+
const versions = [...existing.versions, version].slice(-MAX_VERSIONS);
|
|
433
|
+
const updated: AgentDocumentRecord = {
|
|
434
|
+
...existing,
|
|
435
|
+
title,
|
|
436
|
+
body,
|
|
437
|
+
tags,
|
|
438
|
+
status: input.status ?? existing.status,
|
|
439
|
+
updatedAt: timestamp,
|
|
440
|
+
versions,
|
|
441
|
+
lastArtifactId: input.lastArtifactId === undefined ? existing.lastArtifactId : input.lastArtifactId || undefined,
|
|
442
|
+
};
|
|
443
|
+
this.writeStore({
|
|
444
|
+
...store,
|
|
445
|
+
documents: store.documents.map((document) => document.id === existing.id ? updated : document),
|
|
446
|
+
});
|
|
447
|
+
return updated;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
public updateArtifactId(idOrTitle: string, artifactId: string): AgentDocumentRecord {
|
|
451
|
+
const store = this.readStore();
|
|
452
|
+
const existing = this.findInStore(store, idOrTitle);
|
|
453
|
+
if (!existing) throw new Error(`Unknown document ${idOrTitle}`);
|
|
454
|
+
const updated: AgentDocumentRecord = {
|
|
455
|
+
...existing,
|
|
456
|
+
lastArtifactId: artifactId,
|
|
457
|
+
updatedAt: nowIso(),
|
|
458
|
+
};
|
|
459
|
+
this.writeStore({
|
|
460
|
+
...store,
|
|
461
|
+
documents: store.documents.map((document) => document.id === existing.id ? updated : document),
|
|
462
|
+
});
|
|
463
|
+
return updated;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
public addComment(idOrTitle: string, input: AgentDocumentCommentInput): AgentDocumentRecord {
|
|
467
|
+
const store = this.readStore();
|
|
468
|
+
const existing = this.findInStore(store, idOrTitle);
|
|
469
|
+
if (!existing) throw new Error(`Unknown document ${idOrTitle}`);
|
|
470
|
+
const body = input.body.trim();
|
|
471
|
+
if (!body) throw new Error('Comment body is required.');
|
|
472
|
+
assertDocumentContentSafe([body]);
|
|
473
|
+
const timestamp = nowIso();
|
|
474
|
+
const comment: AgentDocumentComment = {
|
|
475
|
+
id: this.nextCommentId(existing.comments),
|
|
476
|
+
body,
|
|
477
|
+
status: 'open',
|
|
478
|
+
createdAt: timestamp,
|
|
479
|
+
updatedAt: timestamp,
|
|
480
|
+
};
|
|
481
|
+
const updated: AgentDocumentRecord = {
|
|
482
|
+
...existing,
|
|
483
|
+
comments: [...existing.comments, comment],
|
|
484
|
+
updatedAt: timestamp,
|
|
485
|
+
};
|
|
486
|
+
this.writeStore({
|
|
487
|
+
...store,
|
|
488
|
+
documents: store.documents.map((document) => document.id === existing.id ? updated : document),
|
|
489
|
+
});
|
|
490
|
+
return updated;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
public resolveComment(idOrTitle: string, commentId: string): AgentDocumentRecord {
|
|
494
|
+
const store = this.readStore();
|
|
495
|
+
const existing = this.findInStore(store, idOrTitle);
|
|
496
|
+
if (!existing) throw new Error(`Unknown document ${idOrTitle}`);
|
|
497
|
+
const lookup = commentId.trim().toLowerCase();
|
|
498
|
+
if (!lookup) throw new Error('commentId is required.');
|
|
499
|
+
const target = existing.comments.find((comment) => comment.id.toLowerCase() === lookup);
|
|
500
|
+
if (!target) throw new Error(`Unknown document comment ${commentId}`);
|
|
501
|
+
const timestamp = nowIso();
|
|
502
|
+
const updated: AgentDocumentRecord = {
|
|
503
|
+
...existing,
|
|
504
|
+
updatedAt: timestamp,
|
|
505
|
+
comments: existing.comments.map((comment) => comment.id === target.id
|
|
506
|
+
? { ...comment, status: 'resolved', updatedAt: timestamp, resolvedAt: timestamp }
|
|
507
|
+
: comment),
|
|
508
|
+
};
|
|
509
|
+
this.writeStore({
|
|
510
|
+
...store,
|
|
511
|
+
documents: store.documents.map((document) => document.id === existing.id ? updated : document),
|
|
512
|
+
});
|
|
513
|
+
return updated;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
public suggestUpdate(idOrTitle: string, input: AgentDocumentSuggestionInput): AgentDocumentRecord {
|
|
517
|
+
const store = this.readStore();
|
|
518
|
+
const existing = this.findInStore(store, idOrTitle);
|
|
519
|
+
if (!existing) throw new Error(`Unknown document ${idOrTitle}`);
|
|
520
|
+
const title = input.title === undefined ? existing.title : normalizeTitle(input.title);
|
|
521
|
+
const body = input.body.trim();
|
|
522
|
+
this.validateRequired(title, body);
|
|
523
|
+
const tags = input.tags === undefined ? existing.tags : normalizeList(input.tags);
|
|
524
|
+
const summary = input.summary?.trim() || 'AI suggested revision.';
|
|
525
|
+
const rationale = input.rationale?.trim() || 'No rationale provided.';
|
|
526
|
+
assertDocumentContentSafe([title, body, summary, rationale, ...tags]);
|
|
527
|
+
const timestamp = nowIso();
|
|
528
|
+
const suggestion: AgentDocumentSuggestion = {
|
|
529
|
+
id: this.nextSuggestionId(existing.suggestions),
|
|
530
|
+
title,
|
|
531
|
+
body,
|
|
532
|
+
tags,
|
|
533
|
+
...(input.status === undefined ? {} : { documentStatus: input.status }),
|
|
534
|
+
summary,
|
|
535
|
+
rationale,
|
|
536
|
+
status: 'proposed',
|
|
537
|
+
createdAt: timestamp,
|
|
538
|
+
updatedAt: timestamp,
|
|
539
|
+
};
|
|
540
|
+
const updated: AgentDocumentRecord = {
|
|
541
|
+
...existing,
|
|
542
|
+
suggestions: [...existing.suggestions, suggestion],
|
|
543
|
+
updatedAt: timestamp,
|
|
544
|
+
};
|
|
545
|
+
this.writeStore({
|
|
546
|
+
...store,
|
|
547
|
+
documents: store.documents.map((document) => document.id === existing.id ? updated : document),
|
|
548
|
+
});
|
|
549
|
+
return updated;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
public acceptSuggestion(idOrTitle: string, suggestionId: string): AgentDocumentRecord {
|
|
553
|
+
const store = this.readStore();
|
|
554
|
+
const existing = this.findInStore(store, idOrTitle);
|
|
555
|
+
if (!existing) throw new Error(`Unknown document ${idOrTitle}`);
|
|
556
|
+
const target = this.findSuggestion(existing, suggestionId);
|
|
557
|
+
if (!target) throw new Error(`Unknown document suggestion ${suggestionId}`);
|
|
558
|
+
if (target.status === 'accepted') return existing;
|
|
559
|
+
if (target.status === 'rejected') throw new Error(`Document suggestion ${target.id} is already rejected.`);
|
|
560
|
+
const timestamp = nowIso();
|
|
561
|
+
const version: AgentDocumentVersion = {
|
|
562
|
+
id: `v${existing.versions.length + 1}`,
|
|
563
|
+
title: target.title,
|
|
564
|
+
body: target.body,
|
|
565
|
+
summary: target.summary,
|
|
566
|
+
createdAt: timestamp,
|
|
567
|
+
};
|
|
568
|
+
const updated: AgentDocumentRecord = {
|
|
569
|
+
...existing,
|
|
570
|
+
title: target.title,
|
|
571
|
+
body: target.body,
|
|
572
|
+
tags: [...target.tags],
|
|
573
|
+
status: target.documentStatus ?? existing.status,
|
|
574
|
+
updatedAt: timestamp,
|
|
575
|
+
versions: [...existing.versions, version].slice(-MAX_VERSIONS),
|
|
576
|
+
suggestions: existing.suggestions.map((suggestion) => suggestion.id === target.id
|
|
577
|
+
? { ...suggestion, status: 'accepted', updatedAt: timestamp, resolvedAt: timestamp }
|
|
578
|
+
: suggestion),
|
|
579
|
+
};
|
|
580
|
+
this.writeStore({
|
|
581
|
+
...store,
|
|
582
|
+
documents: store.documents.map((document) => document.id === existing.id ? updated : document),
|
|
583
|
+
});
|
|
584
|
+
return updated;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
public rejectSuggestion(idOrTitle: string, suggestionId: string): AgentDocumentRecord {
|
|
588
|
+
const store = this.readStore();
|
|
589
|
+
const existing = this.findInStore(store, idOrTitle);
|
|
590
|
+
if (!existing) throw new Error(`Unknown document ${idOrTitle}`);
|
|
591
|
+
const target = this.findSuggestion(existing, suggestionId);
|
|
592
|
+
if (!target) throw new Error(`Unknown document suggestion ${suggestionId}`);
|
|
593
|
+
if (target.status === 'accepted') throw new Error(`Document suggestion ${target.id} is already accepted.`);
|
|
594
|
+
if (target.status === 'rejected') return existing;
|
|
595
|
+
const timestamp = nowIso();
|
|
596
|
+
const updated: AgentDocumentRecord = {
|
|
597
|
+
...existing,
|
|
598
|
+
updatedAt: timestamp,
|
|
599
|
+
suggestions: existing.suggestions.map((suggestion) => suggestion.id === target.id
|
|
600
|
+
? { ...suggestion, status: 'rejected', updatedAt: timestamp, resolvedAt: timestamp }
|
|
601
|
+
: suggestion),
|
|
602
|
+
};
|
|
603
|
+
this.writeStore({
|
|
604
|
+
...store,
|
|
605
|
+
documents: store.documents.map((document) => document.id === existing.id ? updated : document),
|
|
606
|
+
});
|
|
607
|
+
return updated;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
public attachArtifact(idOrTitle: string, input: AgentDocumentAttachmentInput): AgentDocumentRecord {
|
|
611
|
+
const store = this.readStore();
|
|
612
|
+
const existing = this.findInStore(store, idOrTitle);
|
|
613
|
+
if (!existing) throw new Error(`Unknown document ${idOrTitle}`);
|
|
614
|
+
const artifactId = input.artifactId.trim();
|
|
615
|
+
if (!artifactId) throw new Error('artifactId is required.');
|
|
616
|
+
const label = normalizeTitle(input.label?.trim() || input.filename?.trim() || artifactId);
|
|
617
|
+
const note = input.note?.trim() || undefined;
|
|
618
|
+
assertDocumentContentSafe([label, note ?? '']);
|
|
619
|
+
const timestamp = nowIso();
|
|
620
|
+
const duplicate = existing.attachments.find((attachment) => attachment.artifactId.toLowerCase() === artifactId.toLowerCase());
|
|
621
|
+
const snapshot = {
|
|
622
|
+
artifactId,
|
|
623
|
+
label,
|
|
624
|
+
...(note ? { note } : {}),
|
|
625
|
+
...(input.filename?.trim() ? { filename: input.filename.trim() } : {}),
|
|
626
|
+
...(input.mimeType?.trim() ? { mimeType: input.mimeType.trim() } : {}),
|
|
627
|
+
...(input.kind?.trim() ? { kind: input.kind.trim() } : {}),
|
|
628
|
+
...(typeof input.sizeBytes === 'number' && Number.isFinite(input.sizeBytes)
|
|
629
|
+
? { sizeBytes: Math.max(0, Math.trunc(input.sizeBytes)) }
|
|
630
|
+
: {}),
|
|
631
|
+
};
|
|
632
|
+
const attachment: AgentDocumentAttachment = duplicate
|
|
633
|
+
? { ...duplicate, ...snapshot, updatedAt: timestamp }
|
|
634
|
+
: { id: this.nextAttachmentId(existing.attachments), ...snapshot, createdAt: timestamp, updatedAt: timestamp };
|
|
635
|
+
const attachments = duplicate
|
|
636
|
+
? existing.attachments.map((entry) => entry.id === duplicate.id ? attachment : entry)
|
|
637
|
+
: [...existing.attachments, attachment];
|
|
638
|
+
const updated: AgentDocumentRecord = {
|
|
639
|
+
...existing,
|
|
640
|
+
attachments,
|
|
641
|
+
updatedAt: timestamp,
|
|
642
|
+
};
|
|
643
|
+
this.writeStore({
|
|
644
|
+
...store,
|
|
645
|
+
documents: store.documents.map((document) => document.id === existing.id ? updated : document),
|
|
646
|
+
});
|
|
647
|
+
return updated;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
public markReviewed(idOrTitle: string): AgentDocumentRecord {
|
|
651
|
+
return this.update(idOrTitle, { status: 'reviewed', summary: 'Marked reviewed.' });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
public delete(idOrTitle: string): boolean {
|
|
655
|
+
const store = this.readStore();
|
|
656
|
+
const existing = this.findInStore(store, idOrTitle);
|
|
657
|
+
if (!existing) return false;
|
|
658
|
+
this.writeStore({
|
|
659
|
+
...store,
|
|
660
|
+
documents: store.documents.filter((document) => document.id !== existing.id),
|
|
661
|
+
});
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private readStore(): DocumentStoreFile {
|
|
666
|
+
if (!existsSync(this.storePath)) return { version: STORE_VERSION, documents: [] };
|
|
667
|
+
return parseStore(readFileSync(this.storePath, 'utf-8'));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
private writeStore(store: DocumentStoreFile): void {
|
|
671
|
+
mkdirSync(dirname(this.storePath), { recursive: true });
|
|
672
|
+
const tempPath = `${this.storePath}.tmp`;
|
|
673
|
+
writeFileSync(tempPath, formatStore(store), 'utf-8');
|
|
674
|
+
renameSync(tempPath, this.storePath);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
private validateRequired(title: string, body: string): void {
|
|
678
|
+
if (!title) throw new Error('Document title is required.');
|
|
679
|
+
if (!body) throw new Error('Document body is required.');
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
private nextId(title: string, documents: readonly AgentDocumentRecord[]): string {
|
|
683
|
+
const base = slugify(title);
|
|
684
|
+
const existing = new Set(documents.map((document) => document.id));
|
|
685
|
+
if (!existing.has(base)) return base;
|
|
686
|
+
for (let index = 2; index < 10_000; index += 1) {
|
|
687
|
+
const candidate = `${base}-${index}`;
|
|
688
|
+
if (!existing.has(candidate)) return candidate;
|
|
689
|
+
}
|
|
690
|
+
throw new Error('Unable to allocate a unique document id.');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
private nextCommentId(comments: readonly AgentDocumentComment[]): string {
|
|
694
|
+
const existing = new Set(comments.map((comment) => comment.id));
|
|
695
|
+
for (let index = 1; index < 10_000; index += 1) {
|
|
696
|
+
const candidate = `c${index}`;
|
|
697
|
+
if (!existing.has(candidate)) return candidate;
|
|
698
|
+
}
|
|
699
|
+
throw new Error('Unable to allocate a unique document comment id.');
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
private nextSuggestionId(suggestions: readonly AgentDocumentSuggestion[]): string {
|
|
703
|
+
const existing = new Set(suggestions.map((suggestion) => suggestion.id));
|
|
704
|
+
for (let index = 1; index < 10_000; index += 1) {
|
|
705
|
+
const candidate = `s${index}`;
|
|
706
|
+
if (!existing.has(candidate)) return candidate;
|
|
707
|
+
}
|
|
708
|
+
throw new Error('Unable to allocate a unique document suggestion id.');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
private nextAttachmentId(attachments: readonly AgentDocumentAttachment[]): string {
|
|
712
|
+
const existing = new Set(attachments.map((attachment) => attachment.id));
|
|
713
|
+
for (let index = 1; index < 10_000; index += 1) {
|
|
714
|
+
const candidate = `a${index}`;
|
|
715
|
+
if (!existing.has(candidate)) return candidate;
|
|
716
|
+
}
|
|
717
|
+
throw new Error('Unable to allocate a unique document attachment id.');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private findSuggestion(document: AgentDocumentRecord, suggestionId: string): AgentDocumentSuggestion | null {
|
|
721
|
+
const lookup = suggestionId.trim().toLowerCase();
|
|
722
|
+
if (!lookup) throw new Error('suggestionId is required.');
|
|
723
|
+
return document.suggestions.find((suggestion) => suggestion.id.toLowerCase() === lookup) ?? null;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private findInStore(store: DocumentStoreFile, idOrTitle: string): AgentDocumentRecord | null {
|
|
727
|
+
const lookup = idOrTitle.trim().toLowerCase();
|
|
728
|
+
if (!lookup) return null;
|
|
729
|
+
return store.documents.find((document) => (
|
|
730
|
+
document.id.toLowerCase() === lookup
|
|
731
|
+
|| document.title.toLowerCase() === lookup
|
|
732
|
+
)) ?? null;
|
|
733
|
+
}
|
|
734
|
+
}
|