codehere 0.5.0-alpha.6 → 0.6.0-alpha.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.
Files changed (33) hide show
  1. package/README.md +18 -8
  2. package/dist/application/use-cases/generate-narration.js +111 -0
  3. package/dist/application/use-cases/generate-thread-title.js +58 -4
  4. package/dist/application/use-cases/narrator-prompt-template.js +158 -0
  5. package/dist/index.js +75 -6
  6. package/dist/infrastructure/cost/budget-config.js +113 -0
  7. package/dist/infrastructure/cost/spend-aggregator.js +147 -0
  8. package/dist/infrastructure/hooks/claude-sdk-hook.js +280 -4
  9. package/dist/infrastructure/hooks/hook-mode.js +71 -0
  10. package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +43 -0
  11. package/dist/infrastructure/hooks/pre-prompt-scan.js +122 -0
  12. package/dist/infrastructure/orchestration/aider-adapter.js +28 -0
  13. package/dist/infrastructure/orchestration/artifacts-extractor.js +128 -8
  14. package/dist/infrastructure/orchestration/claude-code-adapter.js +1 -0
  15. package/dist/infrastructure/orchestration/codex-adapter.js +14 -0
  16. package/dist/infrastructure/orchestration/failover-classifier.js +151 -0
  17. package/dist/infrastructure/orchestration/failover-policy.js +106 -0
  18. package/dist/infrastructure/orchestration/gemini-adapter.js +116 -14
  19. package/dist/infrastructure/orchestration/opencode-adapter.js +100 -18
  20. package/dist/infrastructure/orchestration/progress-aggregator.js +141 -6
  21. package/dist/infrastructure/orchestration/session-manager.js +95 -9
  22. package/dist/infrastructure/protocols/mcp-server-impl.js +533 -14
  23. package/dist/infrastructure/protocols/ui-page.js +2598 -212
  24. package/dist/infrastructure/security/permissions-config.js +234 -0
  25. package/dist/infrastructure/storage/project-thread-mirror.js +346 -24
  26. package/dist/infrastructure/storage/project-thread-reader.js +23 -1
  27. package/dist/infrastructure/storage/project-thread-title-backfill.js +28 -0
  28. package/dist/presentation/cli/commands/budget-command.js +79 -0
  29. package/dist/presentation/cli/commands/narrator-command.js +101 -0
  30. package/dist/presentation/cli/commands/permission-command.js +116 -0
  31. package/dist/presentation/cli/commands/scan-tool-command.js +36 -7
  32. package/dist/utils/typo-guard.js +77 -17
  33. package/package.json +1 -1
@@ -25,6 +25,9 @@ import { AUDIT_FILE } from '../hooks/claude-sdk-hook.js';
25
25
  import { appendSpawned, appendCompleted, appendFinalReport, loadRecent } from './session-persistence.js';
26
26
  import { generateFinalReport, printFinalReportToTerminal } from '../../application/use-cases/generate-final-report.js';
27
27
  import { mirrorSpawnedToThread, mirrorCompletedToThread } from '../storage/project-thread-mirror.js';
28
+ import { scanPromptBeforeSpawn } from '../hooks/pre-prompt-scan.js';
29
+ import { generateNarration } from '../../application/use-cases/generate-narration.js';
30
+ import { randomUUID } from 'crypto';
28
31
  // ── Agent registry ─────────────────────────────────────────────────────────────
29
32
  // All backends codehere can spawn. Order here drives the default UI dropdown
30
33
  // order. Built-in adapters first; user-config-driven ones appended from
@@ -47,19 +50,25 @@ const REGISTRY = [
47
50
  // Conductor is appended to the same array so UIs that iterate REGISTRY see it.
48
51
  REGISTRY.push(buildConductorAdapter(() => REGISTRY));
49
52
  export function availableAgents() {
50
- return REGISTRY.map((a) => ({ id: a.id, label: a.label, installed: a.isInstalled() }));
53
+ return REGISTRY.map((a) => ({
54
+ id: a.id,
55
+ label: a.label,
56
+ installed: a.isInstalled(),
57
+ ...(a.description ? { description: a.description } : {}),
58
+ }));
51
59
  }
52
60
  /** Per-agent readiness across the full registry. Powers the setup-aware UI. */
53
61
  export function readinessForAllAgents() {
54
62
  return REGISTRY.map((a) => {
55
- if (a.checkReadiness)
56
- return a.checkReadiness();
57
- // Fallback for adapters that haven't implemented checkReadiness yet: treat
58
- // "installed" as "ready", "not installed" as "needs_install without hint".
59
- const installed = a.isInstalled();
60
- return installed
61
- ? { id: a.id, label: a.label, state: 'ready', installed: true }
62
- : { id: a.id, label: a.label, state: 'needs_install', installed: false };
63
+ const base = a.checkReadiness
64
+ ? a.checkReadiness()
65
+ : a.isInstalled()
66
+ ? { id: a.id, label: a.label, state: 'ready', installed: true }
67
+ : { id: a.id, label: a.label, state: 'needs_install', installed: false };
68
+ // v0.6 S5 — mirror the adapter's "when to use me" description into the
69
+ // readiness payload so the dashboard dropdown can surface it as a
70
+ // plain-English hover tooltip without a separate API call.
71
+ return a.description ? { ...base, description: a.description } : base;
63
72
  });
64
73
  }
65
74
  export function getAdapter(id) {
@@ -115,6 +124,14 @@ export class SessionManager {
115
124
  * reads via getFinalReport() through the /api/tasks serializer so non-technical
116
125
  * users see the prose summary without opening a terminal. */
117
126
  finalReportBySession = new Map();
127
+ /** v0.6 M3 · PM Narrator. Mid-task plain-English summaries cached per
128
+ * session so the API can serve the latest narration without re-running
129
+ * the LLM call on every frontend poll. Latest-only — older narrations
130
+ * are overwritten when a new one fires. Per claim_user_persona HIGH:
131
+ * non-technical users read narration, not tool calls. */
132
+ narrationBySession = new Map();
133
+ narrationTimers = new Map();
134
+ narrationStepsAtLastFire = new Map();
118
135
  spawn(prompt, agentId = 'claude-code', options) {
119
136
  const adapter = getAdapter(agentId);
120
137
  if (!adapter) {
@@ -123,6 +140,24 @@ export class SessionManager {
123
140
  if (!adapter.isInstalled()) {
124
141
  throw new Error(`Agent ${adapter.label} (${adapter.id}) is not installed on this machine`);
125
142
  }
143
+ // Cross-vendor pre-prompt scan (task #110, slice 1). Closes the gap
144
+ // the FAQ now claims as "v0.5.x → v0.6 track": every adapter — not
145
+ // just claude-code — gets a safety scan on the user's prompt before
146
+ // spawn. Catches API-key paste, prompt-injection patterns, etc.
147
+ // Always writes one audit entry (synthetic toolName 'pre_prompt');
148
+ // refuses spawn ONLY when getHookMode() === 'block' AND the prompt
149
+ // has critical/high findings. Other modes (advisory/log-only/off)
150
+ // log + proceed.
151
+ const preScanSessionId = randomUUID();
152
+ const scan = scanPromptBeforeSpawn({
153
+ sessionId: preScanSessionId,
154
+ agent: agentId,
155
+ prompt,
156
+ cwd: options?.cwd,
157
+ });
158
+ if (!scan.allowed) {
159
+ throw new Error(scan.blockReason || 'pre-prompt scan blocked the spawn');
160
+ }
126
161
  const session = adapter.spawn(prompt, options);
127
162
  this.sessions.set(session.sessionId, session);
128
163
  // Persistence: write the spawned record immediately so a daemon crash still
@@ -215,10 +250,61 @@ export class SessionManager {
215
250
  .catch(() => {
216
251
  // generateFinalReport never throws by contract; defensive only.
217
252
  });
253
+ // v0.6 M3 — clean up the narration timer for this session.
254
+ const t = this.narrationTimers.get(session.sessionId);
255
+ if (t) {
256
+ clearInterval(t);
257
+ this.narrationTimers.delete(session.sessionId);
258
+ }
218
259
  unsubscribe();
219
260
  });
261
+ // v0.6 M3 — PM Narrator. Mid-task plain-English summaries fired
262
+ // every 30s while the session is running. Heuristic: only fire if
263
+ // the step count grew by ≥3 since the last narration (otherwise
264
+ // we'd narrate the same state with new words). Fire-and-forget;
265
+ // failures (no claude binary, timeout, malformed output) silently
266
+ // skip — narration is enrichment, not the primary surface.
267
+ this.startNarrationTimer(session);
220
268
  return session;
221
269
  }
270
+ /** v0.6 M3 — periodic narration loop per session. */
271
+ startNarrationTimer(session) {
272
+ if (process.env.CODEHERE_DISABLE_NARRATOR === '1')
273
+ return;
274
+ this.narrationStepsAtLastFire.set(session.sessionId, 0);
275
+ const intervalMs = Number(process.env.CODEHERE_NARRATOR_INTERVAL_MS ?? 30_000);
276
+ const minStepDelta = Number(process.env.CODEHERE_NARRATOR_MIN_STEPS ?? 3);
277
+ const timer = setInterval(() => {
278
+ // Skip if session has settled.
279
+ if (session.status !== 'running')
280
+ return;
281
+ const steps = session.steps ?? [];
282
+ const lastFireSteps = this.narrationStepsAtLastFire.get(session.sessionId) ?? 0;
283
+ if (steps.length - lastFireSteps < minStepDelta)
284
+ return;
285
+ // Mark BEFORE the async call to prevent double-firing if the LLM
286
+ // is slow.
287
+ this.narrationStepsAtLastFire.set(session.sessionId, steps.length);
288
+ const elapsedMs = Date.now() - session.startedAt;
289
+ void generateNarration({
290
+ sessionId: session.sessionId,
291
+ prompt: session.prompt,
292
+ agent: session.agent,
293
+ elapsedMs,
294
+ steps: [...steps],
295
+ })
296
+ .then((out) => {
297
+ if (out)
298
+ this.narrationBySession.set(session.sessionId, out);
299
+ })
300
+ .catch(() => { });
301
+ }, intervalMs);
302
+ this.narrationTimers.set(session.sessionId, timer);
303
+ }
304
+ /** v0.6 M3 — latest narration for a session, or undefined. */
305
+ getNarration(sessionId) {
306
+ return this.narrationBySession.get(sessionId);
307
+ }
222
308
  /** Persisted (read-only) sessions from prior daemon runs. Cached at boot. */
223
309
  listPersisted() {
224
310
  return this.persistedCache;