@pellux/goodvibes-agent 1.5.6 → 1.5.8
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 +13 -0
- package/README.md +1 -1
- package/dist/package/main.js +1431 -1219
- package/package.json +1 -1
- package/src/agent/memory-prompt.ts +3 -3
- package/src/agent/operator-actions.ts +1 -1
- package/src/agent/prompt-context-receipts.ts +7 -5
- package/src/agent/session-registration.ts +1 -1
- package/src/agent/vibe-file.ts +87 -6
- package/src/cli/local-library-command-shared.ts +1 -1
- package/src/cli/memory-command.ts +9 -6
- package/src/cli/resume-relaunch-notice.ts +1 -1
- package/src/cli/tui-startup.ts +1 -1
- package/src/config/credential-status.ts +165 -0
- package/src/config/index.ts +12 -0
- package/src/config/secrets.ts +17 -0
- package/src/core/conversation-rendering.ts +2 -2
- package/src/core/system-message-noise.ts +3 -3
- package/src/core/thinking-overlay.ts +1 -1
- package/src/input/agent-workspace-basic-command-editor-submission.ts +1 -1
- package/src/input/agent-workspace-basic-command-editors.ts +1 -1
- package/src/input/agent-workspace-calendar-connect-editor.ts +1 -1
- package/src/input/agent-workspace-calendar-oauth-editor.ts +1 -1
- package/src/input/agent-workspace-calendar-subscribe-editor.ts +1 -1
- package/src/input/agent-workspace-command-editor-engine.ts +2 -2
- package/src/input/agent-workspace-direct-editor-submission.ts +1 -1
- package/src/input/agent-workspace-email-connect-editor.ts +1 -1
- package/src/input/agent-workspace-live-counters.ts +1 -1
- package/src/input/agent-workspace-settings.ts +1 -1
- package/src/input/agent-workspace-snapshot-builders.ts +2 -2
- package/src/input/agent-workspace-snapshot-config.ts +1 -1
- package/src/input/agent-workspace-snapshot.ts +9 -9
- package/src/input/agent-workspace-types.ts +4 -4
- package/src/input/agent-workspace.ts +1 -1
- package/src/input/commands/calendar-connect-runtime.ts +1 -1
- package/src/input/commands/calendar-subscription-runtime.ts +2 -2
- package/src/input/commands/operator-actions-runtime.ts +1 -1
- package/src/input/commands/session-content.ts +1 -1
- package/src/input/commands/session-workflow.ts +1 -1
- package/src/input/feed-context-factory.ts +4 -4
- package/src/input/handler-feed.ts +9 -9
- package/src/input/handler.ts +1 -1
- package/src/input/panel-paste-flood-guard.ts +3 -3
- package/src/input/settings-modal.ts +1 -1
- package/src/main.ts +4 -4
- package/src/renderer/agent-workspace-context-lines.ts +1 -1
- package/src/renderer/startup-theme-probe.ts +1 -1
- package/src/renderer/status-glyphs.ts +2 -2
- package/src/renderer/terminal-bg-probe.ts +1 -1
- package/src/renderer/terminal-escapes.ts +3 -2
- package/src/renderer/theme-mode-config.ts +2 -2
- package/src/renderer/theme.ts +4 -4
- package/src/renderer/thinking.ts +1 -1
- package/src/renderer/ui-factory.ts +1 -1
- package/src/renderer/ui-primitives.ts +2 -2
- package/src/runtime/bootstrap-core.ts +32 -1
- package/src/runtime/bootstrap.ts +3 -3
- package/src/runtime/calendar-boot-refresh.ts +1 -1
- package/src/runtime/lan-scan-consent.ts +1 -1
- package/src/runtime/services.ts +7 -7
- package/src/runtime/terminal-output-guard.ts +1 -1
- package/src/runtime/ui-services.ts +1 -1
- package/src/shell/agent-workspace-fullscreen.ts +1 -1
- package/src/shell/terminal-focus-mode.ts +9 -9
- package/src/shell/ui-openers.ts +1 -1
- package/src/tools/agent-harness-operator-methods.ts +1 -1
- package/src/tools/agent-harness-personal-ops-discovery.ts +2 -2
- package/src/tools/agent-harness-personal-ops-lanes.ts +1 -1
- package/src/tools/agent-harness-personal-ops-types.ts +1 -1
- package/src/tools/agent-harness-prompt-context.ts +7 -4
- package/src/tools/agent-local-registry-memory.ts +1 -1
- package/src/tools/agent-operator-method-tool.ts +1 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-agent",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "GoodVibes personal operator assistant TUI with a proactive Agent product brain, isolated Agent Knowledge, local profiles, routines, skills, personas, and explicit build delegation.",
|
|
6
6
|
"type": "module",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MemoryRecord, MemoryRegistry, MemoryVectorStats } from '@pellux/goodvibes-sdk/platform/state';
|
|
2
|
-
//
|
|
2
|
+
// The recall-honesty floor + eligibility receipt now live in the SDK as
|
|
3
3
|
// the ONE cross-surface contract (memory-recall-contract.ts) instead of being defined
|
|
4
4
|
// per surface. This file re-exports them unchanged so every existing agent consumer
|
|
5
5
|
// keeps importing from './memory-prompt.ts', while the SDK is the single source of the
|
|
@@ -32,9 +32,9 @@ function formatMemoryLine(record: MemoryRecord): string {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Per-turn semantic ranking of an already-eligible memory set
|
|
35
|
+
* Per-turn semantic ranking of an already-eligible memory set.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
37
|
+
* Eligibility (confidence + reviewState + provenance) shipped as the hard trust
|
|
38
38
|
* gate but had no per-turn query to rank WITHIN that eligible set — records were only
|
|
39
39
|
* ever ordered by stored confidence/recency, regardless of whether they had anything to
|
|
40
40
|
* do with what the user actually just asked. `rankMemoryForTurn` never touches the gate
|
|
@@ -103,7 +103,7 @@ export const OPERATOR_ACTIONS: Record<OperatorActionId, OperatorActionDescriptor
|
|
|
103
103
|
pathTemplate: '/api/automation/jobs/{jobId}/run',
|
|
104
104
|
targetField: 'jobId',
|
|
105
105
|
},
|
|
106
|
-
//
|
|
106
|
+
// automation.jobs.pause/resume were retired (redundant with
|
|
107
107
|
// disable/enable — same {id,enabled} output, same semantics). The
|
|
108
108
|
// user-facing "pause"/"resume" verb is unchanged; only the wire action +
|
|
109
109
|
// path moved to the canonical disable/enable methods.
|
|
@@ -9,7 +9,7 @@ import { AgentPersonaRegistry, buildActivePersonaPrompt } from './persona-regist
|
|
|
9
9
|
import { buildProjectContextPrompt, discoverProjectContextFiles } from './project-context-files.ts';
|
|
10
10
|
import { AgentRoutineRegistry, buildEnabledRoutinesPrompt, evaluateAgentRoutineReadiness } from './routine-registry.ts';
|
|
11
11
|
import { AgentSkillRegistry, buildEnabledSkillsPrompt, evaluateAgentSkillBundleReadiness, evaluateAgentSkillReadiness } from './skill-registry.ts';
|
|
12
|
-
import {
|
|
12
|
+
import { buildVibeProjectionPrompt, discoverVibeFiles } from './vibe-file.ts';
|
|
13
13
|
|
|
14
14
|
export type PromptContextReceiptStatus = 'active' | 'attention' | 'empty' | 'unavailable';
|
|
15
15
|
export type PromptContextReceiptSource = 'turn' | 'follow_up' | 'manual';
|
|
@@ -140,7 +140,9 @@ function receiptSegment(input: Omit<PromptContextReceiptSegment, 'approxTokens'>
|
|
|
140
140
|
|
|
141
141
|
function buildRuntimePromptReceiptSegments(input: RuntimePromptCompositionInput): readonly PromptContextReceiptSegment[] {
|
|
142
142
|
const vibe = discoverVibeFiles(input.shellPaths);
|
|
143
|
-
|
|
143
|
+
// The VIBE prompt is a PROJECTION of persona/constraint records, not a
|
|
144
|
+
// re-read of the files (discoverVibeFiles above stays for the file-discovery receipt).
|
|
145
|
+
const vibePrompt = buildVibeProjectionPrompt(input.memoryRegistry) ?? '';
|
|
144
146
|
const projectContext = discoverProjectContextFiles(input.shellPaths);
|
|
145
147
|
const projectContextPrompt = buildProjectContextPrompt(input.shellPaths) ?? '';
|
|
146
148
|
const memoryRecords = input.memoryRegistry.getAll();
|
|
@@ -232,7 +234,7 @@ function buildRuntimePromptReceiptSegments(input: RuntimePromptCompositionInput)
|
|
|
232
234
|
suppressedCount: suppressedMemory.length,
|
|
233
235
|
promptChars: memoryPrompt.length,
|
|
234
236
|
promptText: memoryPrompt,
|
|
235
|
-
// Honest degrade note
|
|
237
|
+
// Honest degrade note: when per-turn relevance scoring did not
|
|
236
238
|
// run — no active-turn text, semantic index unavailable, or no vector match —
|
|
237
239
|
// say so instead of silently presenting the fallback confidence/recency order as
|
|
238
240
|
// if it were a relevance ranking.
|
|
@@ -243,7 +245,7 @@ function buildRuntimePromptReceiptSegments(input: RuntimePromptCompositionInput)
|
|
|
243
245
|
class: record.cls,
|
|
244
246
|
confidence: record.confidence,
|
|
245
247
|
reason: describeMemoryPromptEligibility(record).reason,
|
|
246
|
-
// Per-turn relevance
|
|
248
|
+
// Per-turn relevance: honest wording, only present when actually scored.
|
|
247
249
|
// F7a: the raw percent is paired with a qualitative band (see relevanceBand)
|
|
248
250
|
// so a genuinely-lower-but-real score like "28%" doesn't read as noise.
|
|
249
251
|
...(memoryRanking.scored ? { relevance: `relevance to this turn: ${memoryRanking.relevanceById.get(record.id) ?? 0}% (${relevanceBand(memoryRanking.relevanceById.get(record.id) ?? 0)})` } : {}),
|
|
@@ -341,7 +343,7 @@ export function composeRuntimePromptWithReceipt(input: RuntimePromptCompositionI
|
|
|
341
343
|
const supplement = getTierPromptSupplement(tier);
|
|
342
344
|
const prompt = joinPromptParts(
|
|
343
345
|
input.runtimePrompt,
|
|
344
|
-
|
|
346
|
+
buildVibeProjectionPrompt(input.memoryRegistry),
|
|
345
347
|
buildProjectContextPrompt(input.shellPaths),
|
|
346
348
|
input.operatorPolicy,
|
|
347
349
|
buildReviewedMemoryPrompt(input.memoryRegistry, { turnText: input.turnText }),
|
|
@@ -86,7 +86,7 @@ export interface SessionRecordSummary {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
|
-
* Honest outcome discriminator. 'still_closed' is the
|
|
89
|
+
* Honest outcome discriminator. 'still_closed' is the heartbeat-resurrection
|
|
90
90
|
* guard: a 200 that carries conflict:{status:'closed'} with reopened:false MUST NOT
|
|
91
91
|
* be read as 'registered' or 'reopened'.
|
|
92
92
|
*/
|
package/src/agent/vibe-file.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
4
|
import type { ShellPathService } from '@/runtime/index.ts';
|
|
4
5
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
5
6
|
import { assertNoSecretLikeText } from './persona-registry.ts';
|
|
6
7
|
import { parseMarkdownFrontmatter, stripMarkdownFrontmatter } from './markdown-frontmatter.ts';
|
|
7
|
-
//
|
|
8
|
+
// VIBE.md is now a PROJECTION of persona/constraint records in the
|
|
8
9
|
// canonical memory store, not a separate source of truth. renderVibeProjection emits
|
|
9
10
|
// the same '## VIBE.md' block from those records (caveat preserved); the file is
|
|
10
11
|
// demoted to an import/export FORMAT folded in via vibeBodyToConstraintOptions.
|
|
11
12
|
import { renderVibeProjection, vibeBodyToConstraintOptions } from '@pellux/goodvibes-sdk/platform/state';
|
|
12
|
-
import type { MemoryRegistry, MemoryScope } from '@pellux/goodvibes-sdk/platform/state';
|
|
13
|
+
import type { MemoryRecord, MemoryRegistry, MemoryScope } from '@pellux/goodvibes-sdk/platform/state';
|
|
13
14
|
|
|
14
15
|
export type AgentVibeScope = 'project' | 'global';
|
|
15
16
|
|
|
@@ -212,7 +213,7 @@ export function buildVibePrompt(shellPaths: AgentVibePaths): string | null {
|
|
|
212
213
|
}
|
|
213
214
|
|
|
214
215
|
/**
|
|
215
|
-
*
|
|
216
|
+
* The VIBE.md prompt block as a PROJECTION of persona/constraint records.
|
|
216
217
|
*
|
|
217
218
|
* This is the store-sourced replacement for buildVibePrompt (which reads the file
|
|
218
219
|
* directly). It renders the same '## GoodVibes Agent VIBE.md' block — including the
|
|
@@ -220,12 +221,12 @@ export function buildVibePrompt(shellPaths: AgentVibePaths): string | null {
|
|
|
220
221
|
* store, so persona instructions have a single source of truth alongside every other
|
|
221
222
|
* durable fact. Returns null when there are no persona records to project.
|
|
222
223
|
*/
|
|
223
|
-
export function buildVibeProjectionPrompt(
|
|
224
|
-
return renderVibeProjection(
|
|
224
|
+
export function buildVibeProjectionPrompt(memoryRecords: { getAll(): readonly MemoryRecord[] }): string | null {
|
|
225
|
+
return renderVibeProjection(memoryRecords.getAll());
|
|
225
226
|
}
|
|
226
227
|
|
|
227
228
|
/**
|
|
228
|
-
*
|
|
229
|
+
* Fold discovered VIBE.md files into the store as persona/constraint
|
|
229
230
|
* records — the file demoted to an IMPORT FORMAT. Each bullet becomes one record so a
|
|
230
231
|
* later single-record edit changes exactly one projected line. Secret-like content is
|
|
231
232
|
* already rejected by discoverVibeFiles (readVibeCandidate → assertNoSecretLikeText),
|
|
@@ -255,6 +256,86 @@ export async function importVibeFilesIntoMemory(
|
|
|
255
256
|
return created;
|
|
256
257
|
}
|
|
257
258
|
|
|
259
|
+
/**
|
|
260
|
+
* The persisted marker that makes the VIBE.md → memory import a strictly
|
|
261
|
+
* ONE-TIME migration. Keyed by absolute file path → content hash, so importing the same
|
|
262
|
+
* VIBE.md twice is a no-op (re-import would create near-duplicate persona records), while
|
|
263
|
+
* a NEW project's VIBE.md still migrates exactly once. Mirrors the sessions.spine-folded
|
|
264
|
+
* marker precedent (bootstrap.ts) — a small JSON sidecar, read before and written after.
|
|
265
|
+
*/
|
|
266
|
+
interface VibeImportMarker {
|
|
267
|
+
readonly migrated: Record<string, string>;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function vibeImportMarkerPath(shellPaths: Pick<ShellPathService, 'resolveUserPath'>): string {
|
|
271
|
+
return shellPaths.resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'vibe-import.migrated.json');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function readVibeImportMarker(path: string): VibeImportMarker {
|
|
275
|
+
if (!existsSync(path)) return { migrated: {} };
|
|
276
|
+
try {
|
|
277
|
+
const parsed = JSON.parse(readFileSync(path, 'utf-8')) as unknown;
|
|
278
|
+
const migrated = (parsed as { migrated?: unknown })?.migrated;
|
|
279
|
+
if (migrated && typeof migrated === 'object' && !Array.isArray(migrated)) {
|
|
280
|
+
const out: Record<string, string> = {};
|
|
281
|
+
for (const [key, value] of Object.entries(migrated as Record<string, unknown>)) {
|
|
282
|
+
if (typeof value === 'string') out[key] = value;
|
|
283
|
+
}
|
|
284
|
+
return { migrated: out };
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
// Corrupt marker → treat as "nothing migrated"; the path+hash guard still prevents
|
|
288
|
+
// duplicating any record that was already imported in a prior clean run.
|
|
289
|
+
}
|
|
290
|
+
return { migrated: {} };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function writeVibeImportMarker(path: string, marker: VibeImportMarker): void {
|
|
294
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
295
|
+
writeFileSync(path, `${JSON.stringify(marker, null, 2)}\n`, 'utf-8');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function hashVibeBody(body: string): string {
|
|
299
|
+
return createHash('sha256').update(body, 'utf-8').digest('hex');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Fold discovered VIBE.md files into persona/constraint records ONCE.
|
|
304
|
+
* Guarded by a persisted path→hash marker so it never re-imports the same file (which
|
|
305
|
+
* would create near-duplicate persona records). Called at boot AFTER memoryStore.init().
|
|
306
|
+
* Returns the number of records created this run (0 when everything is already migrated).
|
|
307
|
+
*/
|
|
308
|
+
export async function importVibeFilesIntoMemoryOnce(
|
|
309
|
+
memoryRegistry: MemoryRegistry,
|
|
310
|
+
shellPaths: AgentVibePaths,
|
|
311
|
+
): Promise<number> {
|
|
312
|
+
const markerPath = vibeImportMarkerPath(shellPaths);
|
|
313
|
+
const marker = readVibeImportMarker(markerPath);
|
|
314
|
+
const snapshot = discoverVibeFiles(shellPaths);
|
|
315
|
+
let created = 0;
|
|
316
|
+
let changed = false;
|
|
317
|
+
for (const file of snapshot.files) {
|
|
318
|
+
const key = resolve(file.path);
|
|
319
|
+
const hash = hashVibeBody(file.body);
|
|
320
|
+
if (marker.migrated[key] === hash) continue; // already migrated this exact content
|
|
321
|
+
const scope: MemoryScope = file.scope === 'global' ? 'team' : 'project';
|
|
322
|
+
const name = file.frontmatter.name?.trim();
|
|
323
|
+
const options = vibeBodyToConstraintOptions(file.body, {
|
|
324
|
+
scope,
|
|
325
|
+
...(name ? { name } : {}),
|
|
326
|
+
sourceRef: file.path,
|
|
327
|
+
});
|
|
328
|
+
for (const opts of options) {
|
|
329
|
+
await memoryRegistry.add(opts);
|
|
330
|
+
created += 1;
|
|
331
|
+
}
|
|
332
|
+
marker.migrated[key] = hash;
|
|
333
|
+
changed = true;
|
|
334
|
+
}
|
|
335
|
+
if (changed) writeVibeImportMarker(markerPath, marker);
|
|
336
|
+
return created;
|
|
337
|
+
}
|
|
338
|
+
|
|
258
339
|
export function formatVibeStatus(snapshot: AgentVibeSnapshot): string {
|
|
259
340
|
const lines: string[] = [
|
|
260
341
|
'GoodVibes Agent VIBE.md',
|
|
@@ -7,7 +7,7 @@ import type { CliCommandRuntime } from './management.ts';
|
|
|
7
7
|
/**
|
|
8
8
|
* Shared prelude for the Agent-local library CLI commands (personas, skills,
|
|
9
9
|
* skill bundles): option parsing, success/failure envelopes, and registry
|
|
10
|
-
* accessors. Split out of local-library-command.ts (
|
|
10
|
+
* accessors. Split out of local-library-command.ts (the file
|
|
11
11
|
* cleanly contained three independent command handlers glued together) so
|
|
12
12
|
* personas-command.ts, skills-command.ts, and skill-bundle-command.ts can
|
|
13
13
|
* share one option-parsing/output prelude without duplicating it.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, resolve } from 'node:path';
|
|
3
|
-
import { createShellPathService } from '@/runtime/index.ts';
|
|
4
3
|
import {
|
|
5
4
|
MemoryEmbeddingProviderRegistry,
|
|
6
5
|
MemoryRegistry,
|
|
7
6
|
MemoryStore,
|
|
7
|
+
resolveCanonicalMemoryDbPath,
|
|
8
8
|
type MemoryBundle,
|
|
9
9
|
type MemoryClass,
|
|
10
10
|
type MemoryLink,
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
} from '@pellux/goodvibes-sdk/platform/state';
|
|
19
19
|
import { assertNoSecretLikeMemoryText } from '../agent/memory-safety.ts';
|
|
20
20
|
import { formatAgentRecordReference, formatAgentRecordReviewState } from '../agent/record-labels.ts';
|
|
21
|
-
import { GOODVIBES_AGENT_SURFACE_ROOT } from '../config/surface.ts';
|
|
22
21
|
import type { CliCommandOutput } from './types.ts';
|
|
23
22
|
import type { CliCommandRuntime } from './management.ts';
|
|
24
23
|
|
|
@@ -196,11 +195,15 @@ function timestamp(value: number): string {
|
|
|
196
195
|
return new Date(value).toISOString().slice(0, 19).replace('T', ' ');
|
|
197
196
|
}
|
|
198
197
|
|
|
198
|
+
// The CLI no longer opens a private per-surface agent/memory.sqlite. It opens the
|
|
199
|
+
// ONE canonical cross-surface store (~/.goodvibes/shared/memory.sqlite) so a memory
|
|
200
|
+
// added via `goodvibes-agent memory add` is visible to the runtime, the TUI, and the
|
|
201
|
+
// SDK — and vice-versa. The old CLI-written store is folded into the canonical store
|
|
202
|
+
// (loss-free, idempotent) by the runtime's foldAgentLegacyMemory at boot, since that
|
|
203
|
+
// fold already sources shellPaths.resolveUserPath('agent', 'memory.sqlite') — the exact
|
|
204
|
+
// path this function used to write to.
|
|
199
205
|
function memoryDbPath(runtime: CliCommandRuntime): string {
|
|
200
|
-
return
|
|
201
|
-
workingDirectory: runtime.workingDirectory,
|
|
202
|
-
homeDirectory: runtime.homeDirectory,
|
|
203
|
-
}).resolveUserPath(GOODVIBES_AGENT_SURFACE_ROOT, 'memory.sqlite');
|
|
206
|
+
return resolveCanonicalMemoryDbPath(runtime.homeDirectory);
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
async function withMemory<T>(runtime: CliCommandRuntime, fn: (context: MemoryContext) => Promise<T> | T): Promise<T> {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SessionManager } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* RESUME-ON-RELAUNCH
|
|
4
|
+
* RESUME-ON-RELAUNCH.
|
|
5
5
|
*
|
|
6
6
|
* The dogfood finding: relaunching the agent after a normal (non-crash) exit
|
|
7
7
|
* silently starts fresh with no offer or notice about the prior session — the
|
package/src/cli/tui-startup.ts
CHANGED
|
@@ -116,7 +116,7 @@ export function applyInitialTuiCliState(options: {
|
|
|
116
116
|
// Normal relaunch: onboarding is done and the user didn't ask for
|
|
117
117
|
// onboarding or an explicit `sessions resume`. Surface an honest,
|
|
118
118
|
// non-blocking resume affordance instead of silently starting fresh
|
|
119
|
-
// (
|
|
119
|
+
// (a dogfood finding) — never auto-resume, declining is
|
|
120
120
|
// frictionless (just start typing).
|
|
121
121
|
surfaceResumeRelaunchNotice({
|
|
122
122
|
getLastSessionPointer: () => readLastSessionPointer({
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credential-status.ts — client-side, secret-FREE credential STATUS read.
|
|
3
|
+
*
|
|
4
|
+
* When GoodVibes Agent acts as a CLIENT of an adopted
|
|
5
|
+
* external daemon, it reads credential *status* (configured / usable) from the
|
|
6
|
+
* daemon's shared store over the wire — the `credentials.get` operator method
|
|
7
|
+
* (GET /config/credentials, admin + read:config). It NEVER receives raw secret
|
|
8
|
+
* bytes: the wire contract (CREDENTIALS_SNAPSHOT_SCHEMA) carries status metadata
|
|
9
|
+
* only. This is a VISIBILITY path, not secret transport.
|
|
10
|
+
*
|
|
11
|
+
* Host-vs-client (see the SDK decision record 2026-07-06-config-sharing): a surface
|
|
12
|
+
* that IS the daemon host reads its own local SecretsManager directly (no wire hop —
|
|
13
|
+
* src/config/secrets.ts). A surface acting as a daemon client reads STATUS here.
|
|
14
|
+
* Secret RESOLUTION (the value provider auth needs) always stays local/env; only the
|
|
15
|
+
* status read moves to the daemon path.
|
|
16
|
+
*
|
|
17
|
+
* The degrade contract mirrors goodvibes-webui v1.0.1 `deriveCredentialAvailability`
|
|
18
|
+
* exactly: a 503 CREDENTIAL_STORE_UNAVAILABLE (by machine code), a METHOD_NOT_FOUND
|
|
19
|
+
* from an older daemon, or any transport failure yields an honest reason-carrying
|
|
20
|
+
* `available: false` — NEVER a fabricated "configured", NEVER a surfaced secret byte.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** One credential's status metadata from the daemon's shared store — never bytes. */
|
|
24
|
+
export interface CredentialStatusEntry {
|
|
25
|
+
readonly key: string;
|
|
26
|
+
readonly configured: boolean;
|
|
27
|
+
readonly usable: boolean;
|
|
28
|
+
readonly source?: string;
|
|
29
|
+
readonly scope?: string;
|
|
30
|
+
readonly secure?: boolean;
|
|
31
|
+
readonly overriddenByEnv?: boolean;
|
|
32
|
+
readonly refSource?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type CredentialAvailability =
|
|
36
|
+
| { readonly available: true; readonly credentials: readonly CredentialStatusEntry[] }
|
|
37
|
+
| { readonly available: false; readonly reason: string };
|
|
38
|
+
|
|
39
|
+
/** Minimal connection shape for the daemon-client status read. */
|
|
40
|
+
export interface CredentialStatusConnection {
|
|
41
|
+
readonly baseUrl: string;
|
|
42
|
+
readonly token: string | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
46
|
+
|
|
47
|
+
const CREDENTIALS_ROUTE = '/config/credentials';
|
|
48
|
+
const CREDENTIAL_STATUS_TIMEOUT_MS = 1500;
|
|
49
|
+
|
|
50
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
51
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function firstString(record: Record<string, unknown>, keys: readonly string[]): string {
|
|
55
|
+
for (const key of keys) {
|
|
56
|
+
const item = record[key];
|
|
57
|
+
if (typeof item === 'string' && item.trim()) return item;
|
|
58
|
+
}
|
|
59
|
+
return '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Fold a `credentials.get` outcome into an honest availability value. Pure — this is
|
|
64
|
+
* the exact degrade contract mirrored from webui v1.0.1: keyed on the MACHINE CODE, it
|
|
65
|
+
* never fabricates "configured" and never carries a secret value. A malformed body (no
|
|
66
|
+
* credentials array) is treated as unavailable, not silently empty-but-configured.
|
|
67
|
+
*/
|
|
68
|
+
export function deriveCredentialAvailability(
|
|
69
|
+
outcome: { ok: true; value: unknown } | { ok: false; error: unknown },
|
|
70
|
+
): CredentialAvailability {
|
|
71
|
+
if (!outcome.ok) {
|
|
72
|
+
const err = asRecord(outcome.error);
|
|
73
|
+
const code = err ? firstString(err, ['code']) : '';
|
|
74
|
+
if (code === 'CREDENTIAL_STORE_UNAVAILABLE') {
|
|
75
|
+
return { available: false, reason: 'The daemon has no shared credential store wired.' };
|
|
76
|
+
}
|
|
77
|
+
if (code === 'METHOD_NOT_FOUND' || code === 'NOT_INVOKABLE') {
|
|
78
|
+
return { available: false, reason: 'This daemon does not serve credential status yet.' };
|
|
79
|
+
}
|
|
80
|
+
return { available: false, reason: 'Credential status unavailable right now.' };
|
|
81
|
+
}
|
|
82
|
+
const value = asRecord(outcome.value);
|
|
83
|
+
const raw = value?.credentials;
|
|
84
|
+
if (!Array.isArray(raw)) return { available: false, reason: 'Credential status unavailable right now.' };
|
|
85
|
+
const credentials: CredentialStatusEntry[] = [];
|
|
86
|
+
for (const item of raw) {
|
|
87
|
+
const rec = asRecord(item);
|
|
88
|
+
const key = rec ? firstString(rec, ['key']) : '';
|
|
89
|
+
if (!rec || !key) continue;
|
|
90
|
+
credentials.push({
|
|
91
|
+
key,
|
|
92
|
+
configured: rec.configured === true,
|
|
93
|
+
usable: rec.usable === true,
|
|
94
|
+
source: firstString(rec, ['source']) || undefined,
|
|
95
|
+
scope: firstString(rec, ['scope']) || undefined,
|
|
96
|
+
secure: rec.secure === true ? true : rec.secure === false ? false : undefined,
|
|
97
|
+
overriddenByEnv: rec.overriddenByEnv === true ? true : rec.overriddenByEnv === false ? false : undefined,
|
|
98
|
+
refSource: firstString(rec, ['refSource']) || undefined,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return { available: true, credentials };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extract the machine code from a non-2xx daemon response body, falling back to the
|
|
106
|
+
* HTTP status for legacy daemons that predate the code field. A 404 with no code is
|
|
107
|
+
* treated as METHOD_NOT_FOUND (older daemon without the route — same as webui's
|
|
108
|
+
* isMethodUnavailableError); a 503 with no code as CREDENTIAL_STORE_UNAVAILABLE.
|
|
109
|
+
*/
|
|
110
|
+
function machineCodeFromResponse(body: unknown, status: number): string {
|
|
111
|
+
const record = asRecord(body);
|
|
112
|
+
const direct = record ? firstString(record, ['code']) : '';
|
|
113
|
+
if (direct) return direct;
|
|
114
|
+
const nested = record ? asRecord(record.error) : null;
|
|
115
|
+
const nestedCode = nested ? firstString(nested, ['code']) : '';
|
|
116
|
+
if (nestedCode) return nestedCode;
|
|
117
|
+
if (status === 404) return 'METHOD_NOT_FOUND';
|
|
118
|
+
if (status === 503) return 'CREDENTIAL_STORE_UNAVAILABLE';
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read credential status from the connected daemon as a client, honestly degraded.
|
|
124
|
+
* Status only — the response is folded through {@link deriveCredentialAvailability},
|
|
125
|
+
* so a down store, an older daemon, an absent token, or a transport failure all become
|
|
126
|
+
* a reason-carrying `available: false`, never a fabricated "configured" and never a
|
|
127
|
+
* secret byte. `key` narrows to a single credential (the daemon's caller-named probe).
|
|
128
|
+
*/
|
|
129
|
+
export async function fetchDaemonCredentialAvailability(
|
|
130
|
+
connection: CredentialStatusConnection,
|
|
131
|
+
options: { readonly key?: string; readonly fetchImpl?: FetchLike } = {},
|
|
132
|
+
): Promise<CredentialAvailability> {
|
|
133
|
+
if (!connection.token) {
|
|
134
|
+
return { available: false, reason: 'No connected-host operator token; credential status is unavailable.' };
|
|
135
|
+
}
|
|
136
|
+
const fetchImpl = options.fetchImpl ?? (globalThis.fetch as FetchLike | undefined);
|
|
137
|
+
if (!fetchImpl) {
|
|
138
|
+
return { available: false, reason: 'Credential status unavailable right now.' };
|
|
139
|
+
}
|
|
140
|
+
const query = options.key ? `?key=${encodeURIComponent(options.key)}` : '';
|
|
141
|
+
const url = `${connection.baseUrl}${CREDENTIALS_ROUTE}${query}`;
|
|
142
|
+
try {
|
|
143
|
+
const response = await fetchImpl(url, {
|
|
144
|
+
method: 'GET',
|
|
145
|
+
headers: { authorization: `Bearer ${connection.token}` },
|
|
146
|
+
signal: AbortSignal.timeout(CREDENTIAL_STATUS_TIMEOUT_MS),
|
|
147
|
+
});
|
|
148
|
+
const text = await response.text();
|
|
149
|
+
let body: unknown = text;
|
|
150
|
+
try {
|
|
151
|
+
body = text.trim() ? JSON.parse(text) as unknown : {};
|
|
152
|
+
} catch {
|
|
153
|
+
body = text;
|
|
154
|
+
}
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
return deriveCredentialAvailability({
|
|
157
|
+
ok: false,
|
|
158
|
+
error: { code: machineCodeFromResponse(body, response.status), status: response.status },
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return deriveCredentialAvailability({ ok: true, value: body });
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return deriveCredentialAvailability({ ok: false, error });
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -55,3 +55,15 @@ export function getConfiguredSystemPrompt(configManager: Pick<ConfigManager, 'ge
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export { getConfiguredApiKeys, resolveApiKeys } from '@pellux/goodvibes-sdk/platform/config';
|
|
58
|
+
|
|
59
|
+
// The daemon-client credential STATUS read (secret-free, honest-degrade).
|
|
60
|
+
// Value reads above stay local/env; only the status VISIBILITY path moves to the daemon.
|
|
61
|
+
export {
|
|
62
|
+
deriveCredentialAvailability,
|
|
63
|
+
fetchDaemonCredentialAvailability,
|
|
64
|
+
} from './credential-status.ts';
|
|
65
|
+
export type {
|
|
66
|
+
CredentialAvailability,
|
|
67
|
+
CredentialStatusConnection,
|
|
68
|
+
CredentialStatusEntry,
|
|
69
|
+
} from './credential-status.ts';
|
package/src/config/secrets.ts
CHANGED
|
@@ -16,6 +16,23 @@ import {
|
|
|
16
16
|
import { isSecretRefInput } from '@pellux/goodvibes-sdk/platform/config';
|
|
17
17
|
import { GOODVIBES_AGENT_SURFACE_ROOT } from './surface.ts';
|
|
18
18
|
|
|
19
|
+
// Host-vs-client split: this SecretsManager is the LOCAL-HOST
|
|
20
|
+
// read path — pinned to GOODVIBES_AGENT_SURFACE_ROOT, it resolves secret VALUES from the
|
|
21
|
+
// surface store/env for provider auth, unchanged. When the Agent acts as a CLIENT of an
|
|
22
|
+
// adopted external daemon, credential *status* (configured/usable — never bytes) is read
|
|
23
|
+
// over the wire via ./credential-status.ts (`fetchDaemonCredentialAvailability`), which
|
|
24
|
+
// degrades honestly and never fabricates "configured". Only STATUS visibility moves to
|
|
25
|
+
// the daemon path; value resolution stays here, local and env-only for API keys.
|
|
26
|
+
export {
|
|
27
|
+
deriveCredentialAvailability,
|
|
28
|
+
fetchDaemonCredentialAvailability,
|
|
29
|
+
} from './credential-status.ts';
|
|
30
|
+
export type {
|
|
31
|
+
CredentialAvailability,
|
|
32
|
+
CredentialStatusConnection,
|
|
33
|
+
CredentialStatusEntry,
|
|
34
|
+
} from './credential-status.ts';
|
|
35
|
+
|
|
19
36
|
export type SecretsManagerOptions = Omit<SdkSecretsManagerOptions, 'surfaceRoot'>;
|
|
20
37
|
|
|
21
38
|
const RAW_SECRET_LITERAL_PREFIX = '__GOODVIBES_LITERAL_V1__';
|
|
@@ -123,8 +123,8 @@ export function renderConversationAssistantMessage(
|
|
|
123
123
|
// `numWidth=6` (fits 999,999 lines, but wastes 3-4 gutter columns on typical
|
|
124
124
|
// messages) or rendering the numbered output into a scratch buffer and trimming.
|
|
125
125
|
// Neither is clearly better than the current two-pass measurement approach.
|
|
126
|
-
//
|
|
127
|
-
// numbers are enabled" was inaccurate:
|
|
126
|
+
// An earlier commit message's claim that this "eliminates double-parse when line
|
|
127
|
+
// numbers are enabled" was inaccurate: that commit eliminated the legacy
|
|
128
128
|
// `renderMarkdown()` duplicate used for code-block line-number mode ('code').
|
|
129
129
|
// The 'all' mode double-call is a deliberate design choice and remains unchanged.
|
|
130
130
|
const measureWidth = showAllLineNumbers ? width : 0;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Ported from the TUI (src/core/system-message-noise.ts). The TUI regexes are
|
|
10
10
|
* kept VERBATIM (provider-replay fold / agents-snapshot drop / stale-replay
|
|
11
11
|
* drop); the only agent-specific addition is the `[Terminal] Captured …` rule
|
|
12
|
-
* (
|
|
12
|
+
* (the stdout-capture leak). Dropped noise is
|
|
13
13
|
* drop-from-the-feed, NOT delete: the captured-write detail stays reachable via
|
|
14
14
|
* the activity log (the terminal-output guard logs each intercept + the count).
|
|
15
15
|
*/
|
|
@@ -46,7 +46,7 @@ const REPLAY_CHAIN_RE = /WRFC chain (\S+) transitioned .* waiting for action/;
|
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Matches the terminal-output guard's aggregate captured-write notice
|
|
49
|
-
* (
|
|
49
|
+
* (agent-specific). First-run plumbing writes a burst of direct stdout
|
|
50
50
|
* that the guard intercepts and would otherwise summarize into the Recent feed;
|
|
51
51
|
* the count stays reachable in the activity log, so the feed copy is dropped.
|
|
52
52
|
*/
|
|
@@ -57,7 +57,7 @@ const TERMINAL_CAPTURED_RE = /^\[Terminal\] Captured \d+ direct /;
|
|
|
57
57
|
* lookup, so it is trivially testable.
|
|
58
58
|
*/
|
|
59
59
|
export function classifyNoise(message: string, deps: NoiseGateDeps): NoiseVerdict {
|
|
60
|
-
//
|
|
60
|
+
// Agent-specific — the terminal-output guard's aggregate captured-write notice.
|
|
61
61
|
// Kept out of the Recent feed; the detail stays in the activity log.
|
|
62
62
|
if (TERMINAL_CAPTURED_RE.test(message)) {
|
|
63
63
|
return DROP;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* thinking-overlay.ts
|
|
2
|
+
* thinking-overlay.ts — the thinking-indicator overlay + its honest
|
|
3
3
|
* stall clock, extracted from main.ts's render loop.
|
|
4
4
|
*
|
|
5
5
|
* The SDK orchestrator surfaces no lastDeltaAtMs / reconnect signal directly, so
|
|
@@ -42,7 +42,7 @@ function unconfirmed(editor: AgentWorkspaceLocalEditor, message: string, status:
|
|
|
42
42
|
/**
|
|
43
43
|
* The submission data table for the kinds this module builds itself (delegate-task and
|
|
44
44
|
* workplan/notify/secret/profile kinds keep dispatching to their own sibling submission
|
|
45
|
-
* modules below, unchanged).
|
|
45
|
+
* modules below, unchanged). Split out of the single ~570-line
|
|
46
46
|
* `buildAgentWorkspaceBasicCommandEditorSubmission` if-chain, mirroring the
|
|
47
47
|
* construction-side split in agent-workspace-basic-command-editors.ts.
|
|
48
48
|
*/
|
|
@@ -127,7 +127,7 @@ function modelPinUnpinSpec(kind: AgentWorkspaceBasicOwnCommandEditorKind): Agent
|
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
129
|
* The field-spec data table for the kinds this module builds itself (the rest are
|
|
130
|
-
* delegated to sibling domain modules above).
|
|
130
|
+
* delegated to sibling domain modules above). Split out of the single
|
|
131
131
|
* ~600-line `createAgentWorkspaceBasicCommandEditor` if-chain into a data table, the
|
|
132
132
|
* same shape used by every other agent-workspace command-editor domain.
|
|
133
133
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Calendar connect card
|
|
2
|
+
* Calendar connect card — promotes the "Calendar workflows" workspace
|
|
3
3
|
* card from a dead guidance card into a real, dispatchable action.
|
|
4
4
|
*
|
|
5
5
|
* GROUNDED: unlike email, there is no external calendar account/CalDAV
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Calendar OAuth advanced-credentials wizard
|
|
2
|
+
* Calendar OAuth advanced-credentials wizard.
|
|
3
3
|
*
|
|
4
4
|
* The DEFAULT connect experience needs no card at all: `/calendar connect google`
|
|
5
5
|
* (or `outlook`) uses the bundled project client id + PKCE and opens the browser.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Calendar subscribe wizard
|
|
2
|
+
* Calendar subscribe wizard — the "Calendar workflows" card's real
|
|
3
3
|
* connect flow. Subscribes to an external calendar by its iCalendar feed URL
|
|
4
4
|
* (Google "secret address", Outlook published .ics, or any .ics URL), READ-ONLY.
|
|
5
5
|
*
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// one place. Each domain keeps its own field content and command-building logic
|
|
10
10
|
// (real product value, not boilerplate) as entries in a `Record<Kind, ...>` table.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
// identified by
|
|
12
|
+
// Consolidates the ~26 near-identical editor+submission file pairs
|
|
13
|
+
// identified by an earlier design ruling into this engine + per-domain data tables.
|
|
14
14
|
import type { AgentWorkspaceActionResult, AgentWorkspaceEditorField, AgentWorkspaceEditorKind, AgentWorkspaceLocalEditor } from './agent-workspace-types.ts';
|
|
15
15
|
import { quoteSlashCommandArg } from './slash-command-parser.ts';
|
|
16
16
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* generic command-editor dispatch pipeline (agent-workspace-command-editor.ts).
|
|
5
5
|
* Centralizing the dispatch here keeps AgentWorkspace.submitLocalEditor()
|
|
6
6
|
* under the architecture line cap as new direct-action editor kinds are
|
|
7
|
-
* added (
|
|
7
|
+
* added (email-connect-wizard alongside the existing
|
|
8
8
|
* subscription-login-start/finish/logout kinds).
|
|
9
9
|
*/
|
|
10
10
|
import type { CommandContext } from './command-registry.ts';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Email connect wizard
|
|
2
|
+
* Email connect wizard — promotes the "Inbox workflows" workspace
|
|
3
3
|
* card from a dead guidance card into a real, stepped connect flow.
|
|
4
4
|
*
|
|
5
5
|
* Security: the raw password NEVER travels through the generic slash-command
|
|
@@ -10,7 +10,7 @@ export interface SyncAgentWorkspaceLiveCountersOptions {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Quiet, render-path refresh of the two counters
|
|
13
|
+
* Quiet, render-path refresh of the two counters a dogfood finding
|
|
14
14
|
* named as stale: the memory count and the routine counts (incl. each
|
|
15
15
|
* routine's live start count). Call this once per repaint, before rendering,
|
|
16
16
|
* so an external disk mutation (another shell deleting a memory, a CLI
|