@pellux/goodvibes-agent 1.4.4 → 1.5.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/CHANGELOG.md +22 -0
- package/README.md +52 -7
- package/dist/package/main.js +7491 -12253
- package/docs/README.md +2 -2
- package/docs/channels-remote-and-api.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/release-and-publishing.md +1 -1
- package/docs/tools-and-commands.md +5 -5
- package/package.json +4 -2
- package/release/live-verification/live-verification.json +13 -13
- package/release/live-verification/live-verification.md +15 -15
- package/release/performance-snapshot.json +2 -2
- package/release/release-notes.md +5 -7
- package/release/release-readiness.json +6 -6
- package/src/agent/behavior-discovery-summary.ts +4 -18
- package/src/agent/calendar-registry.ts +322 -0
- package/src/agent/competitive-feature-inventory.ts +268 -130
- package/src/agent/document-registry.ts +5 -1
- package/src/agent/email/email-service.ts +350 -0
- package/src/agent/email/imap-client.ts +596 -0
- package/src/agent/email/smtp-client.ts +453 -0
- package/src/agent/ics-calendar.ts +662 -0
- package/src/agent/ics-timezone.ts +172 -0
- package/src/agent/markdown-frontmatter.ts +31 -0
- package/src/agent/memory-safety.ts +1 -1
- package/src/agent/note-registry.ts +3 -9
- package/src/agent/persona-discovery.ts +3 -15
- package/src/agent/persona-registry.ts +1 -10
- package/src/agent/research-source-registry.ts +5 -1
- package/src/agent/routine-discovery.ts +3 -15
- package/src/agent/runtime-profile.ts +3 -0
- package/src/agent/skill-discovery.ts +3 -15
- package/src/agent/skill-draft-proposer.ts +203 -0
- package/src/agent/skill-draft-runner.ts +201 -0
- package/src/agent/skill-registry.ts +36 -1
- package/src/agent/skill-standard.ts +99 -0
- package/src/agent/vibe-file.ts +7 -22
- package/src/cli/completion.ts +1 -1
- package/src/cli/config-overrides.ts +10 -1
- package/src/cli/redaction.ts +17 -5
- package/src/config/provider-model.ts +2 -1
- package/src/config/secret-config.ts +13 -6
- package/src/core/activity-feed.ts +97 -0
- package/src/core/away-digest.ts +161 -0
- package/src/core/conversation-rendering.ts +7 -3
- package/src/core/conversation.ts +22 -15
- package/src/core/hardware-profile.ts +362 -0
- package/src/core/last-seen-store.ts +78 -0
- package/src/core/plain-language.ts +52 -0
- package/src/core/setup-incomplete-hint.ts +90 -0
- package/src/core/system-message-router.ts +38 -87
- package/src/input/agent-workspace-basic-command-editor-submission.ts +60 -220
- package/src/input/agent-workspace-basic-command-editors.ts +39 -141
- package/src/input/agent-workspace-categories.ts +40 -125
- package/src/input/agent-workspace-command-editor.ts +4 -0
- package/src/input/agent-workspace-host-category.ts +0 -5
- package/src/input/agent-workspace-local-editor-submission.ts +3 -1
- package/src/input/agent-workspace-navigation.ts +10 -3
- package/src/input/agent-workspace-onboarding-actions.ts +132 -0
- package/src/input/agent-workspace-onboarding-categories.ts +29 -26
- package/src/input/agent-workspace-onboarding-finish.ts +11 -4
- package/src/input/agent-workspace-onboarding-state.ts +111 -0
- package/src/input/agent-workspace-profile-editor-submission.ts +260 -0
- package/src/input/agent-workspace-profile-editors.ts +155 -0
- package/src/input/agent-workspace-subscription-editor.ts +7 -0
- package/src/input/agent-workspace-types.ts +5 -1
- package/src/input/agent-workspace.ts +65 -39
- package/src/input/command-registry.ts +18 -5
- package/src/input/commands/agent-runtime-profile-runtime.ts +2 -1
- package/src/input/commands/agent-skills-runtime.ts +60 -3
- package/src/input/commands/calendar-runtime.ts +209 -0
- package/src/input/commands/channels-runtime.ts +1 -0
- package/src/input/commands/command-error.ts +9 -0
- package/src/input/commands/compat-runtime.ts +1 -0
- package/src/input/commands/config.ts +1 -0
- package/src/input/commands/conversation-runtime.ts +1 -1
- package/src/input/commands/delegation-runtime.ts +5 -6
- package/src/input/commands/email-runtime.ts +387 -0
- package/src/input/commands/experience-runtime.ts +17 -4
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +6 -1
- package/src/input/commands/knowledge-format.ts +73 -0
- package/src/input/commands/knowledge.ts +9 -74
- package/src/input/commands/local-provider-runtime.ts +2 -1
- package/src/input/commands/local-runtime.ts +10 -4
- package/src/input/commands/mcp-runtime.ts +7 -0
- package/src/input/commands/notify-runtime.ts +17 -1
- package/src/input/commands/onboarding-runtime.ts +1 -0
- package/src/input/commands/operator-actions-runtime.ts +1 -0
- package/src/input/commands/operator-runtime.ts +3 -0
- package/src/input/commands/personas-runtime.ts +1 -0
- package/src/input/commands/platform-access-runtime.ts +40 -17
- package/src/input/commands/product-runtime.ts +6 -1
- package/src/input/commands/provider-accounts-runtime.ts +3 -2
- package/src/input/commands/qrcode-runtime.ts +1 -0
- package/src/input/commands/routines-runtime.ts +1 -0
- package/src/input/commands/runtime-services.ts +0 -13
- package/src/input/commands/security-runtime.ts +1 -0
- package/src/input/commands/session-content.ts +1 -0
- package/src/input/commands/shell-core.ts +5 -2
- package/src/input/commands/subscription-runtime.ts +33 -9
- package/src/input/commands/support-bundle-runtime.ts +8 -1
- package/src/input/commands/tasks-runtime.ts +1 -0
- package/src/input/commands/tts-runtime.ts +1 -0
- package/src/input/commands/vibe-runtime.ts +1 -0
- package/src/input/commands.ts +4 -0
- package/src/input/feed-context-factory.ts +3 -12
- package/src/input/handler-command-route.ts +7 -60
- package/src/input/handler-feed-routes.ts +0 -194
- package/src/input/handler-feed.ts +2 -54
- package/src/input/handler-interactions.ts +0 -2
- package/src/input/handler-modal-stack.ts +0 -9
- package/src/input/handler-picker-routes.ts +24 -1
- package/src/input/handler-shortcuts.ts +11 -40
- package/src/input/handler-ui-state.ts +13 -0
- package/src/input/handler.ts +8 -35
- package/src/input/keybindings.ts +40 -17
- package/src/input/model-picker-local-fit.ts +130 -0
- package/src/input/shell-passthrough.ts +58 -0
- package/src/input/submission-router.ts +0 -5
- package/src/main.ts +129 -90
- package/src/renderer/activity-sidebar.ts +186 -0
- package/src/renderer/agent-workspace-context-lines.ts +5 -48
- package/src/renderer/agent-workspace-style.ts +1 -1
- package/src/renderer/agent-workspace.ts +14 -2
- package/src/renderer/compositor.ts +37 -125
- package/src/renderer/conversation-overlays.ts +3 -3
- package/src/renderer/help-overlay.ts +7 -5
- package/src/renderer/model-workspace.ts +101 -86
- package/src/{panels → renderer}/polish.ts +3 -3
- package/src/renderer/shell-surface.ts +4 -5
- package/src/renderer/status-token.ts +31 -11
- package/src/renderer/tab-strip.ts +1 -1
- package/src/renderer/tool-call.ts +7 -8
- package/src/renderer/tool-labels.ts +92 -0
- package/src/renderer/ui-factory.ts +48 -96
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +6 -15
- package/src/runtime/bootstrap-core.ts +34 -13
- package/src/runtime/bootstrap-hook-bridge.ts +3 -1
- package/src/runtime/bootstrap-shell.ts +3 -50
- package/src/runtime/bootstrap.ts +3 -4
- package/src/runtime/context.ts +1 -1
- package/src/runtime/diagnostics/panels/index.ts +0 -1
- package/src/runtime/diagnostics/panels/policy.ts +1 -1
- package/src/runtime/execution-ledger.ts +16 -1
- package/src/runtime/index.ts +6 -1
- package/src/runtime/onboarding/index.ts +1 -0
- package/src/runtime/onboarding/onboarding-state.ts +200 -0
- package/src/{panels → runtime}/provider-account-snapshot.ts +5 -2
- package/src/runtime/services.ts +16 -4
- package/src/runtime/terminal-output-guard.ts +7 -0
- package/src/runtime/tool-permission-safety.ts +1 -1
- package/src/runtime/ui/model-picker/data-provider.ts +1 -1
- package/src/runtime/ui/model-picker/health-enrichment.ts +2 -2
- package/src/runtime/ui/model-picker/types.ts +2 -2
- package/src/runtime/ui/provider-health/data-provider.ts +3 -3
- package/src/runtime/ui/provider-health/types.ts +2 -2
- package/src/runtime/ui-services.ts +0 -2
- package/src/shell/agent-workspace-fullscreen.ts +0 -1
- package/src/shell/autonomy-surfacing.ts +272 -0
- package/src/shell/session-continuity-hints.ts +0 -6
- package/src/shell/startup-wiring.ts +221 -0
- package/src/shell/ui-openers.ts +18 -29
- package/src/tools/agent-context-policy.ts +1 -1
- package/src/tools/agent-harness-background-processes.ts +4 -4
- package/src/tools/agent-harness-browser-cockpit-route.ts +3 -3
- package/src/tools/agent-harness-command-runner.ts +6 -1
- package/src/tools/agent-harness-document-ops.ts +26 -53
- package/src/tools/agent-harness-execution-history.ts +1 -13
- package/src/tools/agent-harness-execution-posture.ts +0 -15
- package/src/tools/agent-harness-keybinding-metadata.ts +28 -29
- package/src/tools/agent-harness-local-model-cookbook.ts +24 -1
- package/src/tools/agent-harness-metadata.ts +16 -0
- package/src/tools/agent-harness-mode-catalog.ts +2 -9
- package/src/tools/agent-harness-provider-account-metadata.ts +2 -2
- package/src/tools/agent-harness-setup-posture-utils.ts +1 -1
- package/src/tools/agent-harness-tool-schema.ts +13 -14
- package/src/tools/agent-harness-tool.ts +27 -23
- package/src/tools/agent-harness-ui-surface-metadata.ts +10 -20
- package/src/tools/agent-harness-workspace-action-runner.ts +3 -4
- package/src/tools/agent-workspace-tool.ts +2 -33
- package/src/utils/terminal-width.ts +9 -3
- package/src/version.ts +1 -1
- package/src/input/agent-workspace-panel-route.ts +0 -44
- package/src/input/panel-integration-actions.ts +0 -26
- package/src/panels/approval-panel.ts +0 -149
- package/src/panels/automation-control-panel.ts +0 -212
- package/src/panels/base-panel.ts +0 -254
- package/src/panels/builtin/agent.ts +0 -58
- package/src/panels/builtin/knowledge.ts +0 -26
- package/src/panels/builtin/operations.ts +0 -121
- package/src/panels/builtin/session.ts +0 -138
- package/src/panels/builtin/shared.ts +0 -275
- package/src/panels/builtin/usage.ts +0 -21
- package/src/panels/builtin-panels.ts +0 -23
- package/src/panels/confirm-state.ts +0 -61
- package/src/panels/context-visualizer-panel.ts +0 -204
- package/src/panels/cost-tracker-panel.ts +0 -444
- package/src/panels/docs-panel.ts +0 -285
- package/src/panels/index.ts +0 -25
- package/src/panels/knowledge-panel.ts +0 -417
- package/src/panels/memory-panel.ts +0 -226
- package/src/panels/panel-list-panel.ts +0 -464
- package/src/panels/panel-manager.ts +0 -570
- package/src/panels/provider-accounts-panel.ts +0 -233
- package/src/panels/provider-health-domains.ts +0 -208
- package/src/panels/provider-health-panel.ts +0 -720
- package/src/panels/provider-health-tracker.ts +0 -115
- package/src/panels/provider-stats-panel.ts +0 -366
- package/src/panels/qr-panel.ts +0 -207
- package/src/panels/schedule-panel.ts +0 -321
- package/src/panels/scrollable-list-panel.ts +0 -491
- package/src/panels/search-focus.ts +0 -32
- package/src/panels/security-panel.ts +0 -295
- package/src/panels/session-browser-panel.ts +0 -395
- package/src/panels/session-maintenance.ts +0 -125
- package/src/panels/subscription-panel.ts +0 -263
- package/src/panels/system-messages-panel.ts +0 -230
- package/src/panels/tasks-panel.ts +0 -344
- package/src/panels/thinking-panel.ts +0 -304
- package/src/panels/token-budget-panel.ts +0 -475
- package/src/panels/tool-inspector-panel.ts +0 -436
- package/src/panels/types.ts +0 -54
- package/src/renderer/panel-composite.ts +0 -158
- package/src/renderer/panel-tab-bar.ts +0 -69
- package/src/renderer/panel-workspace-bar.ts +0 -42
- package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
- package/src/tools/agent-harness-panel-metadata.ts +0 -211
- /package/src/runtime/perf/{panel-health-monitor.ts → component-health.ts} +0 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import type { CommandContext } from '../input/command-registry.ts';
|
|
4
|
+
import { buildLearningCandidates } from '../tools/agent-harness-learning-curator-proposals.ts';
|
|
5
|
+
import type { AgentSkillRegistry } from './skill-registry.ts';
|
|
6
|
+
import { proposeSkillDrafts } from './skill-draft-proposer.ts';
|
|
7
|
+
import type { SkillDraftPayload } from './skill-draft-proposer.ts';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Ledger types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export interface SkillDraftLedgerEntry {
|
|
14
|
+
/** Stable ledger entry id — matches the persisted skill id assigned by the registry. */
|
|
15
|
+
readonly skillId: string;
|
|
16
|
+
/** Kebab-slug name of the proposed skill. */
|
|
17
|
+
readonly name: string;
|
|
18
|
+
/** Learning candidate that originated this proposal. */
|
|
19
|
+
readonly candidateId: string;
|
|
20
|
+
/** ISO timestamp when the draft was proposed. */
|
|
21
|
+
readonly proposedAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SkillDraftLedgerFile {
|
|
25
|
+
readonly version: 1;
|
|
26
|
+
readonly entries: readonly SkillDraftLedgerEntry[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const LEDGER_VERSION = 1 as const;
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Result type
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
export interface SkillDraftRunResult {
|
|
36
|
+
/** Number of new draft skills persisted this pass. */
|
|
37
|
+
readonly proposed: number;
|
|
38
|
+
/** Number of candidates that were eligible but skipped (dedup / secret). */
|
|
39
|
+
readonly skipped: number;
|
|
40
|
+
/** Entries written to the ledger this pass. */
|
|
41
|
+
readonly entries: readonly SkillDraftLedgerEntry[];
|
|
42
|
+
/** Skill ids created this pass. */
|
|
43
|
+
readonly skillIds: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Ledger I/O
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
51
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readLedger(ledgerPath: string): SkillDraftLedgerFile {
|
|
55
|
+
if (!existsSync(ledgerPath)) return { version: LEDGER_VERSION, entries: [] };
|
|
56
|
+
try {
|
|
57
|
+
const raw: unknown = JSON.parse(readFileSync(ledgerPath, 'utf-8'));
|
|
58
|
+
if (!isRecord(raw)) return { version: LEDGER_VERSION, entries: [] };
|
|
59
|
+
const entries = Array.isArray(raw.entries)
|
|
60
|
+
? (raw.entries as unknown[])
|
|
61
|
+
.filter(isRecord)
|
|
62
|
+
.filter(
|
|
63
|
+
(e): e is Record<string, string> =>
|
|
64
|
+
typeof e.skillId === 'string' &&
|
|
65
|
+
typeof e.name === 'string' &&
|
|
66
|
+
typeof e.candidateId === 'string' &&
|
|
67
|
+
typeof e.proposedAt === 'string',
|
|
68
|
+
)
|
|
69
|
+
.map((e) => ({
|
|
70
|
+
skillId: e.skillId,
|
|
71
|
+
name: e.name,
|
|
72
|
+
candidateId: e.candidateId,
|
|
73
|
+
proposedAt: e.proposedAt,
|
|
74
|
+
} satisfies SkillDraftLedgerEntry))
|
|
75
|
+
: [];
|
|
76
|
+
return { version: LEDGER_VERSION, entries };
|
|
77
|
+
} catch {
|
|
78
|
+
return { version: LEDGER_VERSION, entries: [] };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function writeLedger(ledgerPath: string, file: SkillDraftLedgerFile): void {
|
|
83
|
+
mkdirSync(dirname(ledgerPath), { recursive: true });
|
|
84
|
+
const tmpPath = `${ledgerPath}.tmp`;
|
|
85
|
+
writeFileSync(tmpPath, `${JSON.stringify(file, null, 2)}\n`, 'utf-8');
|
|
86
|
+
renameSync(tmpPath, ledgerPath);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Ledger path resolution
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Derive the ledger path from the registry's skills store path, placing the
|
|
95
|
+
* ledger file as a sibling: `.../skills/skill-draft-ledger.json`.
|
|
96
|
+
*/
|
|
97
|
+
function ledgerPathFromRegistry(registry: AgentSkillRegistry): string {
|
|
98
|
+
// AgentSkillRegistry exposes the store path via snapshot().path
|
|
99
|
+
const snapshot = registry.snapshot();
|
|
100
|
+
return snapshot.path.replace(/skills\.json$/, 'skill-draft-ledger.json');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Runner
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Run one skill-draft-proposal pass.
|
|
109
|
+
*
|
|
110
|
+
* 1. Builds learning candidates from the current CommandContext.
|
|
111
|
+
* 2. Loads existing skill names and ledger (previously proposed names).
|
|
112
|
+
* 3. Calls proposeSkillDrafts (pure).
|
|
113
|
+
* 4. Persists each accepted draft via registry.create().
|
|
114
|
+
* 5. Appends new entries to the ledger.
|
|
115
|
+
*
|
|
116
|
+
* Returns a result summary. Never throws on partial failure — errors for
|
|
117
|
+
* individual proposals are caught so one bad candidate does not abort the pass.
|
|
118
|
+
*/
|
|
119
|
+
export function runSkillDraftProposer(
|
|
120
|
+
context: CommandContext,
|
|
121
|
+
registry: AgentSkillRegistry,
|
|
122
|
+
): SkillDraftRunResult {
|
|
123
|
+
const ledgerPath = ledgerPathFromRegistry(registry);
|
|
124
|
+
const ledger = readLedger(ledgerPath);
|
|
125
|
+
|
|
126
|
+
// Build name sets for dedup
|
|
127
|
+
const existingSkillNames = new Set(
|
|
128
|
+
registry.list().map((s) => s.name.trim().toLowerCase()),
|
|
129
|
+
);
|
|
130
|
+
const previouslyProposedNames = new Set(
|
|
131
|
+
ledger.entries.map((e) => e.name.trim().toLowerCase()),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Gather candidates from the live learning curator
|
|
135
|
+
const candidates = buildLearningCandidates(context);
|
|
136
|
+
|
|
137
|
+
// Count eligible before calling proposer so we can report skipped
|
|
138
|
+
const eligibleCount = candidates.filter(
|
|
139
|
+
(c) =>
|
|
140
|
+
(c.status === 'proposal-ready' || c.status === 'ready-to-promote') &&
|
|
141
|
+
c.proposalTarget === 'skill',
|
|
142
|
+
).length;
|
|
143
|
+
|
|
144
|
+
const drafts = proposeSkillDrafts({ candidates, existingSkillNames, previouslyProposedNames });
|
|
145
|
+
|
|
146
|
+
const newEntries: SkillDraftLedgerEntry[] = [];
|
|
147
|
+
const skillIds: string[] = [];
|
|
148
|
+
|
|
149
|
+
for (const draft of drafts) {
|
|
150
|
+
try {
|
|
151
|
+
const skill = registry.create({
|
|
152
|
+
name: draft.name,
|
|
153
|
+
description: draft.description,
|
|
154
|
+
procedure: draft.procedure,
|
|
155
|
+
triggers: draft.triggers,
|
|
156
|
+
tags: draft.tags,
|
|
157
|
+
requirements: draft.requirements,
|
|
158
|
+
enabled: false, // hard rule: drafts are never auto-enabled
|
|
159
|
+
source: 'agent',
|
|
160
|
+
provenance: draft.provenance,
|
|
161
|
+
});
|
|
162
|
+
const entry: SkillDraftLedgerEntry = {
|
|
163
|
+
skillId: skill.id,
|
|
164
|
+
name: skill.name,
|
|
165
|
+
candidateId: draft.candidateId,
|
|
166
|
+
proposedAt: new Date().toISOString(),
|
|
167
|
+
};
|
|
168
|
+
newEntries.push(entry);
|
|
169
|
+
skillIds.push(skill.id);
|
|
170
|
+
} catch {
|
|
171
|
+
// Individual failures (e.g. name collision race) do not abort the pass
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (newEntries.length > 0) {
|
|
176
|
+
writeLedger(ledgerPath, {
|
|
177
|
+
version: LEDGER_VERSION,
|
|
178
|
+
entries: [...ledger.entries, ...newEntries],
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
proposed: newEntries.length,
|
|
184
|
+
// skipped = eligible candidates that were not persisted (dedup + secret + cap + individual failures)
|
|
185
|
+
skipped: eligibleCount - newEntries.length,
|
|
186
|
+
entries: newEntries,
|
|
187
|
+
skillIds,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Ledger read helper — for harness tool inspection
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Read the current ledger for inspection without running a proposal pass.
|
|
197
|
+
*/
|
|
198
|
+
export function readSkillDraftLedger(registry: AgentSkillRegistry): readonly SkillDraftLedgerEntry[] {
|
|
199
|
+
const ledgerPath = ledgerPathFromRegistry(registry);
|
|
200
|
+
return readLedger(ledgerPath).entries;
|
|
201
|
+
}
|
|
@@ -3,6 +3,7 @@ import { delimiter, dirname, join } from 'node:path';
|
|
|
3
3
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
4
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
5
|
import { assertNoSecretLikeText } from './persona-registry.ts';
|
|
6
|
+
import { parseSkillStandardMarkdown, writeSkillStandardFile } from './skill-standard.ts';
|
|
6
7
|
import { formatAgentRecordReviewState } from './record-labels.ts';
|
|
7
8
|
import type {
|
|
8
9
|
AgentSkillBundleCreateInput,
|
|
@@ -63,6 +64,11 @@ function normalizeName(name: string): string {
|
|
|
63
64
|
return name.trim().replace(/\s+/g, ' ');
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
/** Collapse embedded newlines and whitespace runs so a description is always a single line. */
|
|
68
|
+
function normalizeDescription(description: string): string {
|
|
69
|
+
return description.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
function normalizeList(values: readonly string[] | undefined): string[] {
|
|
67
73
|
const seen = new Set<string>();
|
|
68
74
|
const result: string[] = [];
|
|
@@ -297,7 +303,7 @@ export class AgentSkillRegistry {
|
|
|
297
303
|
public create(input: AgentSkillCreateInput): AgentSkillRecord {
|
|
298
304
|
const store = this.readStore();
|
|
299
305
|
const name = normalizeName(input.name);
|
|
300
|
-
const description = input.description
|
|
306
|
+
const description = normalizeDescription(input.description);
|
|
301
307
|
const procedure = input.procedure.trim();
|
|
302
308
|
this.validateRequired(name, description, procedure);
|
|
303
309
|
const requirements = normalizeRequirements(input.requirements);
|
|
@@ -531,6 +537,35 @@ export class AgentSkillRegistry {
|
|
|
531
537
|
return existing;
|
|
532
538
|
}
|
|
533
539
|
|
|
540
|
+
/**
|
|
541
|
+
* Import a skill from the open skill standard format (SKILL.md).
|
|
542
|
+
* The skill is created with provenance 'skill-standard-import' and enabled=false (review-first policy).
|
|
543
|
+
* Throws if the file content is invalid or contains secret-looking text.
|
|
544
|
+
*/
|
|
545
|
+
public importFromStandard(content: string): AgentSkillRecord {
|
|
546
|
+
const parsed = parseSkillStandardMarkdown(content);
|
|
547
|
+
if ('error' in parsed) throw new Error(parsed.error);
|
|
548
|
+
return this.create({
|
|
549
|
+
name: parsed.name,
|
|
550
|
+
description: parsed.description,
|
|
551
|
+
procedure: parsed.body,
|
|
552
|
+
enabled: false,
|
|
553
|
+
source: 'imported',
|
|
554
|
+
provenance: 'skill-standard-import',
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Export one local skill by id to `<destDir>/<slug>/SKILL.md`.
|
|
560
|
+
* Throws if the skill is not found or the file already exists without overwrite=true.
|
|
561
|
+
* Returns the path written.
|
|
562
|
+
*/
|
|
563
|
+
public exportToStandard(idOrName: string, destDir: string, overwrite = false): string {
|
|
564
|
+
const skill = this.get(idOrName);
|
|
565
|
+
if (!skill) throw new Error(`Unknown skill ${idOrName}`);
|
|
566
|
+
return writeSkillStandardFile(skill, destDir, overwrite);
|
|
567
|
+
}
|
|
568
|
+
|
|
534
569
|
private validateRequired(name: string, description: string, procedure: string): void {
|
|
535
570
|
if (!name) throw new Error('Skill name is required.');
|
|
536
571
|
if (!description) throw new Error('Skill description is required.');
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { assertNoSecretLikeText } from './persona-registry.ts';
|
|
4
|
+
import type { AgentSkillRecord } from './skill-registry-types.ts';
|
|
5
|
+
|
|
6
|
+
export interface SkillStandardParsed {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly description: string;
|
|
9
|
+
readonly body: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SkillStandardParseError {
|
|
13
|
+
readonly error: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseStandardFrontmatter(content: string): Record<string, string> {
|
|
17
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
18
|
+
if (!match) return {};
|
|
19
|
+
const result: Record<string, string> = {};
|
|
20
|
+
for (const line of match[1].split('\n')) {
|
|
21
|
+
const colonIndex = line.indexOf(':');
|
|
22
|
+
if (colonIndex < 1) continue;
|
|
23
|
+
const key = line.slice(0, colonIndex).trim();
|
|
24
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
25
|
+
if (key) result[key] = value;
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse a SKILL.md in the open skill standard format.
|
|
32
|
+
* Frontmatter must contain `name` and `description`; extra keys are tolerated.
|
|
33
|
+
* Returns a parse error if either required key is absent or secret-looking content is found.
|
|
34
|
+
*/
|
|
35
|
+
export function parseSkillStandardMarkdown(content: string): SkillStandardParsed | SkillStandardParseError {
|
|
36
|
+
content = content.replace(/^/, '').replace(/\r\n/g, '\n');
|
|
37
|
+
const frontmatter = parseStandardFrontmatter(content);
|
|
38
|
+
const name = (frontmatter['name'] ?? '').trim();
|
|
39
|
+
const description = (frontmatter['description'] ?? '').trim();
|
|
40
|
+
|
|
41
|
+
if (!name) return { error: 'SKILL.md is missing required frontmatter field: name' };
|
|
42
|
+
if (!description) return { error: 'SKILL.md is missing required frontmatter field: description' };
|
|
43
|
+
|
|
44
|
+
const body = content.replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
assertNoSecretLikeText([name, description, body], 'Shared skill');
|
|
48
|
+
} catch (err) {
|
|
49
|
+
return { error: err instanceof Error ? err.message : 'Secret-looking content rejected.' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { name, description, body };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Render a local skill record to the open skill standard format.
|
|
57
|
+
* Produces `---\nname: ...\ndescription: ...\n---\n<body>\n`
|
|
58
|
+
* Round-trip safe: parse(render(skill)) recovers name, description, body.
|
|
59
|
+
*/
|
|
60
|
+
export function renderSkillStandardMarkdown(
|
|
61
|
+
skill: Pick<AgentSkillRecord, 'name' | 'description' | 'procedure'>,
|
|
62
|
+
): string {
|
|
63
|
+
const nameLine = `name: ${skill.name}`;
|
|
64
|
+
// Collapse any embedded newlines/runs of whitespace so the description
|
|
65
|
+
// never spans more than one line and cannot break YAML frontmatter.
|
|
66
|
+
const safeDescription = skill.description.replace(/[\r\n]+/g, ' ').replace(/\s+/g, ' ').trim();
|
|
67
|
+
const descLine = `description: ${safeDescription}`;
|
|
68
|
+
const body = skill.procedure.trimEnd();
|
|
69
|
+
return `---\n${nameLine}\n${descLine}\n---\n${body}\n`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Write a skill record as a SKILL.md inside `<destDir>/<slug>/SKILL.md`.
|
|
74
|
+
* Throws if the file already exists and `overwrite` is not true.
|
|
75
|
+
* Returns the path that was written.
|
|
76
|
+
*/
|
|
77
|
+
export function writeSkillStandardFile(
|
|
78
|
+
skill: Pick<AgentSkillRecord, 'name' | 'description' | 'procedure'>,
|
|
79
|
+
destDir: string,
|
|
80
|
+
overwrite = false,
|
|
81
|
+
): string {
|
|
82
|
+
const slug = skill.name
|
|
83
|
+
.toLowerCase()
|
|
84
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
85
|
+
.replace(/^-|-$/g, '');
|
|
86
|
+
if (!slug) {
|
|
87
|
+
throw new Error('skill name produces an empty folder name');
|
|
88
|
+
}
|
|
89
|
+
const dir = join(destDir, slug);
|
|
90
|
+
const filePath = join(dir, 'SKILL.md');
|
|
91
|
+
|
|
92
|
+
if (!overwrite && existsSync(filePath)) {
|
|
93
|
+
throw new Error(`Skill file already exists at ${filePath}. Pass --overwrite to replace it.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
mkdirSync(dir, { recursive: true });
|
|
97
|
+
writeFileSync(filePath, renderSkillStandardMarkdown(skill), 'utf-8');
|
|
98
|
+
return filePath;
|
|
99
|
+
}
|
package/src/agent/vibe-file.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
|
3
3
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
4
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
5
|
import { assertNoSecretLikeText } from './persona-registry.ts';
|
|
6
|
+
import { parseMarkdownFrontmatter, stripMarkdownFrontmatter } from './markdown-frontmatter.ts';
|
|
6
7
|
|
|
7
8
|
export type AgentVibeScope = 'project' | 'global';
|
|
8
9
|
|
|
@@ -64,23 +65,6 @@ type AgentVibePaths = Pick<ShellPathService,
|
|
|
64
65
|
'workingDirectory' | 'homeDirectory' | 'resolveUserPath' | 'expandHomePath' | 'resolveWorkspacePath'
|
|
65
66
|
>;
|
|
66
67
|
|
|
67
|
-
function parseFrontmatter(content: string): Record<string, string> {
|
|
68
|
-
const match = content.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
69
|
-
if (!match) return {};
|
|
70
|
-
const result: Record<string, string> = {};
|
|
71
|
-
for (const line of match[1].split('\n')) {
|
|
72
|
-
const [key, ...rest] = line.split(':');
|
|
73
|
-
if (key && rest.length > 0) result[key.trim()] = rest.join(':').trim();
|
|
74
|
-
}
|
|
75
|
-
return result;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function markdownBody(content: string): string {
|
|
79
|
-
const frontmatter = parseFrontmatter(content);
|
|
80
|
-
const body = (frontmatter.system_prompt ?? content.replace(/^---\n[\s\S]*?\n---\n?/, '')).trim();
|
|
81
|
-
return body;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
68
|
function readVibeCandidate(path: string, scope: AgentVibeScope): AgentVibeFile | BlockedAgentVibeFile | null {
|
|
85
69
|
if (!existsSync(path)) return null;
|
|
86
70
|
let content = '';
|
|
@@ -94,8 +78,9 @@ function readVibeCandidate(path: string, scope: AgentVibeScope): AgentVibeFile |
|
|
|
94
78
|
};
|
|
95
79
|
}
|
|
96
80
|
|
|
97
|
-
const
|
|
98
|
-
|
|
81
|
+
const frontmatter = parseMarkdownFrontmatter(content);
|
|
82
|
+
const rawBody = (frontmatter.system_prompt ?? stripMarkdownFrontmatter(content)).trim();
|
|
83
|
+
if (!rawBody) return {
|
|
99
84
|
path,
|
|
100
85
|
scope,
|
|
101
86
|
reason: 'File is empty after frontmatter.',
|
|
@@ -114,9 +99,9 @@ function readVibeCandidate(path: string, scope: AgentVibeScope): AgentVibeFile |
|
|
|
114
99
|
return {
|
|
115
100
|
path,
|
|
116
101
|
scope,
|
|
117
|
-
body:
|
|
118
|
-
frontmatter
|
|
119
|
-
truncated:
|
|
102
|
+
body: rawBody.length > MAX_VIBE_BODY_CHARS ? rawBody.slice(0, MAX_VIBE_BODY_CHARS).trimEnd() : rawBody,
|
|
103
|
+
frontmatter,
|
|
104
|
+
truncated: rawBody.length > MAX_VIBE_BODY_CHARS,
|
|
120
105
|
};
|
|
121
106
|
}
|
|
122
107
|
|
package/src/cli/completion.ts
CHANGED
|
@@ -55,7 +55,7 @@ const OPTIONS = [
|
|
|
55
55
|
'--manual',
|
|
56
56
|
] as const;
|
|
57
57
|
|
|
58
|
-
export function renderCompletion(shell: string | undefined, binary = 'goodvibes'): string {
|
|
58
|
+
export function renderCompletion(shell: string | undefined, binary = 'goodvibes-agent'): string {
|
|
59
59
|
const normalized = (shell ?? 'bash').toLowerCase();
|
|
60
60
|
const words = [...COMMANDS, ...OPTIONS].join(' ');
|
|
61
61
|
|
|
@@ -13,7 +13,14 @@ type RuntimeOnlyConfigManager = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
function mutableConfig(configManager: ConfigManager): Record<string, unknown> {
|
|
16
|
-
|
|
16
|
+
const cast = configManager as unknown as RuntimeOnlyConfigManager;
|
|
17
|
+
if (!cast.config || typeof cast.config !== 'object') {
|
|
18
|
+
throw new Error(
|
|
19
|
+
'ConfigManager.config private field is missing or was renamed in the SDK. ' +
|
|
20
|
+
'Update setRuntimeOnlyConfigValue to use the correct field name.',
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return cast.config;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
function setRuntimeOnlyConfigValue(configManager: ConfigManager, key: ConfigKey, value: unknown): void {
|
|
@@ -158,6 +165,8 @@ export function applyRuntimeFeatureFlagOverrides(
|
|
|
158
165
|
for (const feature of options.disableFeatures) {
|
|
159
166
|
flags[feature] = 'disabled' satisfies PersistedFlagState;
|
|
160
167
|
}
|
|
168
|
+
// Write the entire featureFlags category at once to preserve the exact shape getCategory expects.
|
|
169
|
+
// mutableConfig() validates the 'config' field exists and throws loudly if the SDK renames it.
|
|
161
170
|
mutableConfig(configManager).featureFlags = flags;
|
|
162
171
|
}
|
|
163
172
|
|
package/src/cli/redaction.ts
CHANGED
|
@@ -25,9 +25,14 @@ export interface RedactedConfigResult<T> {
|
|
|
25
25
|
readonly redactedPaths: readonly string[];
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// Redaction rule for sensitive config paths:
|
|
29
|
+
// - Non-string values: redact if truthy (i.e. non-null, non-undefined, non-zero, non-false).
|
|
30
|
+
// Rationale: zero and false are never meaningful secrets; null/undefined mean absent.
|
|
31
|
+
// - String values: redact non-empty strings that are not goodvibes:// secret refs.
|
|
32
|
+
// Rationale: empty string means unset; secret refs are safe placeholders, not raw values.
|
|
28
33
|
function shouldRedactValue(path: string, value: unknown): boolean {
|
|
29
34
|
if (!isSensitiveConfigPath(path)) return false;
|
|
30
|
-
if (typeof value !== 'string') return value !==
|
|
35
|
+
if (typeof value !== 'string') return value !== null && value !== undefined && Boolean(value);
|
|
31
36
|
if (value.trim().length === 0) return false;
|
|
32
37
|
if (value.startsWith('goodvibes://secrets/')) return false;
|
|
33
38
|
return true;
|
|
@@ -64,10 +69,17 @@ export function redactConfig<T>(config: T): RedactedConfigResult<T> {
|
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
export function redactText(input: string): string {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
// Assignment form: keyword=value — anchored so 'monkey=' and 'donkey=' do NOT match.
|
|
73
|
+
// Matches: token=, access_token=, api_key=, api-key=, secret=, password= and colon form token: value
|
|
74
|
+
let output = input
|
|
75
|
+
.replace(
|
|
76
|
+
/(?<![A-Za-z])(?:access_token|api[_-]?key|secret|password|token)\s*=\s*([^ \t\r\n"'`]+)/gi,
|
|
77
|
+
(m, val) => m.slice(0, m.length - val.length) + REDACTED_VALUE,
|
|
78
|
+
)
|
|
79
|
+
.replace(
|
|
80
|
+
/(?<![A-Za-z])(?:access_token|api[_-]?key|secret|password|token)\s*:\s*([^ \t\r\n"'`]+)/gi,
|
|
81
|
+
(m, val) => m.slice(0, m.length - val.length) + REDACTED_VALUE,
|
|
82
|
+
);
|
|
71
83
|
for (const pattern of SECRET_LIKE_TEXT_PATTERNS) {
|
|
72
84
|
output = output.replace(pattern, REDACTED_VALUE);
|
|
73
85
|
}
|
|
@@ -4,7 +4,8 @@ export function getProviderIdFromModel(model: unknown): string {
|
|
|
4
4
|
const raw = String(model ?? '').trim();
|
|
5
5
|
if (!raw) return getProviderIdFromModel(DEFAULT_CONFIG.provider.model);
|
|
6
6
|
const separator = raw.indexOf(':');
|
|
7
|
-
|
|
7
|
+
// separator === 0 means a leading colon (e.g. ':foo') — treat as no provider specified
|
|
8
|
+
return separator > 0 ? raw.slice(0, separator) : getProviderIdFromModel(DEFAULT_CONFIG.provider.model);
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function getModelIdFromProviderModel(model: unknown): string {
|
|
@@ -3,6 +3,8 @@ import type { ConfigKey } from './index.ts';
|
|
|
3
3
|
import type { SecretScope, SecretStorageMedium } from './secrets.ts';
|
|
4
4
|
|
|
5
5
|
export const SECRET_CONFIG_KEYS = new Set<ConfigKey>([
|
|
6
|
+
// email section (app-layer extension, key is string-cast for ConfigKey compatibility)
|
|
7
|
+
'email.passwordRef' as unknown as ConfigKey,
|
|
6
8
|
'surfaces.slack.signingSecret',
|
|
7
9
|
'surfaces.slack.botToken',
|
|
8
10
|
'surfaces.slack.appToken',
|
|
@@ -103,15 +105,20 @@ export async function persistSecretBackedConfigValue(
|
|
|
103
105
|
): Promise<string> {
|
|
104
106
|
const update = buildSecretBackedConfigUpdate(configKey, rawValue);
|
|
105
107
|
const scope = options.scope ?? 'user';
|
|
108
|
+
const medium = getSecretWriteMedium(configManager.get('storage.secretPolicy'));
|
|
109
|
+
|
|
110
|
+
// 1. Validate config write first. If setDynamic throws, no secret is written (avoids orphans).
|
|
111
|
+
configManager.setDynamic(configKey, update.configValue);
|
|
112
|
+
|
|
113
|
+
// 2. Write new secret only after config accepted it.
|
|
106
114
|
if (update.secretKey && update.secretValue !== undefined && secretsManager) {
|
|
107
|
-
await secretsManager.set(update.secretKey, update.secretValue, {
|
|
108
|
-
scope,
|
|
109
|
-
medium: getSecretWriteMedium(configManager.get('storage.secretPolicy')),
|
|
110
|
-
});
|
|
115
|
+
await secretsManager.set(update.secretKey, update.secretValue, { scope, medium });
|
|
111
116
|
}
|
|
117
|
+
|
|
118
|
+
// 3. Clear old secret — pass the same medium so plaintext-medium secrets are found for deletion.
|
|
112
119
|
if (update.clearSecretKey && secretsManager?.delete) {
|
|
113
|
-
await secretsManager.delete(update.clearSecretKey, { scope });
|
|
120
|
+
await secretsManager.delete(update.clearSecretKey, { scope, medium });
|
|
114
121
|
}
|
|
115
|
-
|
|
122
|
+
|
|
116
123
|
return update.configValue;
|
|
117
124
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActivityFeed — the single ambient record of what the assistant has been doing.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the old multi-panel system's SystemMessagesPanel as the sink for
|
|
5
|
+
* operational traffic (model switches, provider discovery, delivery results,
|
|
6
|
+
* background work updates). The Activity sidebar renders the most recent
|
|
7
|
+
* entries; high-priority messages additionally land in the conversation via
|
|
8
|
+
* SystemMessageRouter.
|
|
9
|
+
*
|
|
10
|
+
* Design rules:
|
|
11
|
+
* - Append-only ring buffer (oldest entries dropped past MAX_ENTRIES).
|
|
12
|
+
* - No focus, no selection, no scrolling state — this is a glanceable feed,
|
|
13
|
+
* not an interactive widget. History beyond the visible window is available
|
|
14
|
+
* through the conversation (high priority) and logs.
|
|
15
|
+
* - Subscribers are render invalidators only; they receive no payload.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type ActivityKind =
|
|
19
|
+
| 'status' // runtime/system state changes (model switch, session save)
|
|
20
|
+
| 'tool' // tool activity worth surfacing
|
|
21
|
+
| 'agent' // background agent/delegation updates
|
|
22
|
+
| 'schedule' // scheduled/automation work
|
|
23
|
+
| 'delivery' // channel deliveries (ntfy, Slack, …)
|
|
24
|
+
| 'security' // permission/trust events
|
|
25
|
+
| 'system'; // everything else
|
|
26
|
+
|
|
27
|
+
export type ActivityPriority = 'high' | 'low';
|
|
28
|
+
|
|
29
|
+
export interface ActivityEntry {
|
|
30
|
+
readonly at: number;
|
|
31
|
+
readonly kind: ActivityKind;
|
|
32
|
+
readonly priority: ActivityPriority;
|
|
33
|
+
readonly text: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MAX_ENTRIES = 500;
|
|
37
|
+
|
|
38
|
+
/** Map a bracket-tagged system message (e.g. "[Model] …") to an ActivityKind. */
|
|
39
|
+
export function classifyActivityKind(text: string): ActivityKind {
|
|
40
|
+
const tag = /^\[([^\]]+)\]/.exec(text)?.[1]?.toLowerCase() ?? '';
|
|
41
|
+
if (/(schedule|automation|routine|reminder|watcher)/.test(tag)) return 'schedule';
|
|
42
|
+
if (/(channel|delivery|notify|ntfy|message)/.test(tag)) return 'delivery';
|
|
43
|
+
if (/(agent|delegat|task)/.test(tag)) return 'agent';
|
|
44
|
+
if (/(permission|security|trust|secret)/.test(tag)) return 'security';
|
|
45
|
+
if (/(tool|mcp)/.test(tag)) return 'tool';
|
|
46
|
+
if (/(model|provider|session|memory|recovery|compaction|config|setting)/.test(tag)) return 'status';
|
|
47
|
+
return 'system';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class ActivityFeed {
|
|
51
|
+
private entries: ActivityEntry[] = [];
|
|
52
|
+
private listeners = new Set<() => void>();
|
|
53
|
+
|
|
54
|
+
push(text: string, priority: ActivityPriority = 'low', kind?: ActivityKind): void {
|
|
55
|
+
const trimmed = text.replace(/\s+/g, ' ').trim();
|
|
56
|
+
if (!trimmed) return;
|
|
57
|
+
this.entries.push({
|
|
58
|
+
at: Date.now(),
|
|
59
|
+
kind: kind ?? classifyActivityKind(trimmed),
|
|
60
|
+
priority,
|
|
61
|
+
text: trimmed,
|
|
62
|
+
});
|
|
63
|
+
if (this.entries.length > MAX_ENTRIES) {
|
|
64
|
+
this.entries.splice(0, this.entries.length - MAX_ENTRIES);
|
|
65
|
+
}
|
|
66
|
+
this.notify();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Most recent entries, newest first. */
|
|
70
|
+
latest(limit: number): readonly ActivityEntry[] {
|
|
71
|
+
if (limit <= 0) return [];
|
|
72
|
+
return this.entries.slice(-limit).reverse();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
all(): readonly ActivityEntry[] {
|
|
76
|
+
return this.entries;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
get count(): number {
|
|
80
|
+
return this.entries.length;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
subscribe(listener: () => void): () => void {
|
|
84
|
+
this.listeners.add(listener);
|
|
85
|
+
return () => this.listeners.delete(listener);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private notify(): void {
|
|
89
|
+
for (const listener of this.listeners) {
|
|
90
|
+
try {
|
|
91
|
+
listener();
|
|
92
|
+
} catch {
|
|
93
|
+
// Render invalidators must never break the feed.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|