stagent 0.1.10 → 0.1.12
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/README.md +58 -27
- package/package.json +3 -3
- package/src/__tests__/e2e/blueprint.test.ts +63 -0
- package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
- package/src/__tests__/e2e/helpers.ts +286 -0
- package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
- package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
- package/src/__tests__/e2e/setup.ts +156 -0
- package/src/__tests__/e2e/single-task.test.ts +170 -0
- package/src/app/api/command-palette/recent/route.ts +41 -18
- package/src/app/api/context/batch/route.ts +44 -0
- package/src/app/api/permissions/presets/route.ts +80 -0
- package/src/app/api/playbook/status/route.ts +15 -0
- package/src/app/api/profiles/route.ts +23 -21
- package/src/app/api/settings/pricing/route.ts +15 -0
- package/src/app/costs/page.tsx +53 -43
- package/src/app/globals.css +0 -5
- package/src/app/playbook/[slug]/page.tsx +76 -0
- package/src/app/playbook/page.tsx +54 -0
- package/src/app/profiles/page.tsx +7 -4
- package/src/app/settings/page.tsx +2 -2
- package/src/app/tasks/page.tsx +5 -0
- package/src/components/costs/cost-dashboard.tsx +226 -320
- package/src/components/dashboard/activity-feed.tsx +6 -2
- package/src/components/notifications/batch-proposal-review.tsx +150 -0
- package/src/components/notifications/notification-item.tsx +6 -3
- package/src/components/notifications/pending-approval-host.tsx +57 -11
- package/src/components/playbook/adoption-heatmap.tsx +69 -0
- package/src/components/playbook/journey-card.tsx +110 -0
- package/src/components/playbook/playbook-action-button.tsx +22 -0
- package/src/components/playbook/playbook-browser.tsx +143 -0
- package/src/components/playbook/playbook-card.tsx +102 -0
- package/src/components/playbook/playbook-detail-view.tsx +223 -0
- package/src/components/playbook/playbook-homepage.tsx +142 -0
- package/src/components/playbook/playbook-toc.tsx +90 -0
- package/src/components/playbook/playbook-updated-badge.tsx +23 -0
- package/src/components/playbook/related-docs.tsx +30 -0
- package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
- package/src/components/profiles/context-proposal-review.tsx +7 -3
- package/src/components/profiles/learned-context-panel.tsx +116 -8
- package/src/components/profiles/profile-detail-view.tsx +7 -19
- package/src/components/profiles/profile-form-view.tsx +0 -22
- package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
- package/src/components/settings/api-key-form.tsx +5 -43
- package/src/components/settings/auth-config-section.tsx +10 -6
- package/src/components/settings/auth-status-badge.tsx +8 -0
- package/src/components/settings/budget-guardrails-section.tsx +403 -620
- package/src/components/settings/connection-test-control.tsx +63 -0
- package/src/components/settings/permissions-section.tsx +85 -75
- package/src/components/settings/permissions-sections.tsx +24 -0
- package/src/components/settings/presets-section.tsx +159 -0
- package/src/components/settings/pricing-registry-panel.tsx +164 -0
- package/src/components/shared/app-sidebar.tsx +2 -0
- package/src/components/shared/command-palette.tsx +30 -0
- package/src/components/shared/light-markdown.tsx +134 -0
- package/src/components/workflows/loop-status-view.tsx +8 -4
- package/src/components/workflows/workflow-status-view.tsx +16 -9
- package/src/lib/agents/__tests__/claude-agent.test.ts +7 -2
- package/src/lib/agents/__tests__/learned-context.test.ts +500 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +243 -0
- package/src/lib/agents/__tests__/sweep.test.ts +202 -0
- package/src/lib/agents/claude-agent.ts +104 -78
- package/src/lib/agents/learned-context.ts +32 -28
- package/src/lib/agents/learning-session.ts +234 -0
- package/src/lib/agents/pattern-extractor.ts +34 -64
- package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/general/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/sweep/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +0 -1
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +0 -1
- package/src/lib/agents/profiles/registry.ts +0 -1
- package/src/lib/agents/profiles/sort.ts +7 -0
- package/src/lib/agents/profiles/types.ts +0 -1
- package/src/lib/agents/runtime/catalog.ts +1 -1
- package/src/lib/agents/runtime/claude.ts +66 -0
- package/src/lib/constants/settings.ts +1 -0
- package/src/lib/constants/task-status.ts +6 -0
- package/src/lib/data/seed-data/profiles.ts +0 -3
- package/src/lib/db/schema.ts +3 -0
- package/src/lib/docs/adoption.ts +105 -0
- package/src/lib/docs/journey-tracker.ts +21 -0
- package/src/lib/docs/reader.ts +102 -0
- package/src/lib/docs/types.ts +54 -0
- package/src/lib/docs/usage-stage.ts +60 -0
- package/src/lib/notifications/actionable.ts +18 -10
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
- package/src/lib/settings/budget-guardrails.ts +213 -85
- package/src/lib/settings/permission-presets.ts +150 -0
- package/src/lib/settings/runtime-setup.ts +71 -0
- package/src/lib/usage/__tests__/ledger.test.ts +29 -5
- package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
- package/src/lib/usage/ledger.ts +4 -2
- package/src/lib/usage/pricing-registry.ts +570 -0
- package/src/lib/usage/pricing.ts +15 -41
- package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
- package/src/lib/utils/learned-context-history.ts +150 -0
- package/src/lib/validators/__tests__/profile.test.ts +0 -15
- package/src/lib/validators/__tests__/settings.test.ts +23 -16
- package/src/lib/validators/profile.ts +0 -1
- package/src/lib/validators/settings.ts +3 -9
- package/src/lib/workflows/__tests__/engine.test.ts +2 -0
- package/src/lib/workflows/engine.ts +20 -1
|
@@ -252,6 +252,71 @@ async function runClaudeProfileTests(profileId: string): Promise<ProfileTestRepo
|
|
|
252
252
|
};
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// Lightweight meta-completion (pattern extraction, context summarization, etc.)
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
export async function runMetaCompletion(input: {
|
|
260
|
+
prompt: string;
|
|
261
|
+
activityType: string;
|
|
262
|
+
}): Promise<{ text: string; usage: UsageSnapshot }> {
|
|
263
|
+
const authEnv = await getAuthEnv();
|
|
264
|
+
const startedAt = new Date();
|
|
265
|
+
let usage: UsageSnapshot = {};
|
|
266
|
+
const abortController = new AbortController();
|
|
267
|
+
const timeout = setTimeout(() => abortController.abort(), 60_000);
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const response = query({
|
|
271
|
+
prompt: input.prompt,
|
|
272
|
+
options: {
|
|
273
|
+
abortController,
|
|
274
|
+
includePartialMessages: true,
|
|
275
|
+
cwd: process.cwd(),
|
|
276
|
+
env: buildClaudeSdkEnv(authEnv),
|
|
277
|
+
allowedTools: [],
|
|
278
|
+
maxTurns: 1,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
const collected = await collectResultText(
|
|
283
|
+
response as AsyncIterable<Record<string, unknown>>
|
|
284
|
+
);
|
|
285
|
+
usage = collected.usage;
|
|
286
|
+
|
|
287
|
+
await recordUsageLedgerEntry({
|
|
288
|
+
activityType: input.activityType as import("@/lib/usage/ledger").UsageActivityType,
|
|
289
|
+
runtimeId: "claude-code",
|
|
290
|
+
providerId: "anthropic",
|
|
291
|
+
modelId: usage.modelId ?? null,
|
|
292
|
+
inputTokens: usage.inputTokens ?? null,
|
|
293
|
+
outputTokens: usage.outputTokens ?? null,
|
|
294
|
+
totalTokens: usage.totalTokens ?? null,
|
|
295
|
+
status: "completed",
|
|
296
|
+
startedAt,
|
|
297
|
+
finishedAt: new Date(),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
return { text: collected.resultText, usage };
|
|
301
|
+
} catch (error) {
|
|
302
|
+
await recordUsageLedgerEntry({
|
|
303
|
+
activityType: input.activityType as import("@/lib/usage/ledger").UsageActivityType,
|
|
304
|
+
runtimeId: "claude-code",
|
|
305
|
+
providerId: "anthropic",
|
|
306
|
+
modelId: usage.modelId ?? null,
|
|
307
|
+
inputTokens: usage.inputTokens ?? null,
|
|
308
|
+
outputTokens: usage.outputTokens ?? null,
|
|
309
|
+
totalTokens: usage.totalTokens ?? null,
|
|
310
|
+
status: "failed",
|
|
311
|
+
startedAt,
|
|
312
|
+
finishedAt: new Date(),
|
|
313
|
+
});
|
|
314
|
+
throw error;
|
|
315
|
+
} finally {
|
|
316
|
+
clearTimeout(timeout);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
255
320
|
async function runClaudeTaskAssist(
|
|
256
321
|
input: TaskAssistInput
|
|
257
322
|
): Promise<TaskAssistResponse> {
|
|
@@ -345,6 +410,7 @@ async function testClaudeConnection(): Promise<RuntimeConnectionResult> {
|
|
|
345
410
|
options: {
|
|
346
411
|
abortController,
|
|
347
412
|
maxTurns: 1,
|
|
413
|
+
includePartialMessages: false,
|
|
348
414
|
cwd: process.cwd(),
|
|
349
415
|
env: buildClaudeSdkEnv(authEnv),
|
|
350
416
|
},
|
|
@@ -7,6 +7,7 @@ export const SETTINGS_KEYS = {
|
|
|
7
7
|
PERMISSIONS_ALLOW: "permissions.allow",
|
|
8
8
|
BUDGET_POLICY: "usage.budgetPolicy",
|
|
9
9
|
BUDGET_WARNING_STATE: "usage.budgetWarningState",
|
|
10
|
+
PRICING_REGISTRY: "usage.pricingRegistry",
|
|
10
11
|
} as const;
|
|
11
12
|
|
|
12
13
|
export type AuthMethod = "api_key" | "oauth";
|
|
@@ -47,3 +47,9 @@ export function isValidDragTransition(from: TaskStatus, to: TaskStatus): boolean
|
|
|
47
47
|
|
|
48
48
|
/** Maximum number of times a task can be resumed before requiring a fresh start */
|
|
49
49
|
export const MAX_RESUME_COUNT = 3;
|
|
50
|
+
|
|
51
|
+
/** Default max turns for agent task execution (safety net) */
|
|
52
|
+
export const DEFAULT_MAX_TURNS = 50;
|
|
53
|
+
|
|
54
|
+
/** Default per-execution budget cap in USD */
|
|
55
|
+
export const DEFAULT_MAX_BUDGET_USD = 2.0;
|
|
@@ -34,7 +34,6 @@ export function getSampleProfiles(): SampleProfileSeed[] {
|
|
|
34
34
|
canUseToolPolicy: {
|
|
35
35
|
autoApprove: ["Read", "Grep"],
|
|
36
36
|
},
|
|
37
|
-
temperature: 0.3,
|
|
38
37
|
maxTurns: 18,
|
|
39
38
|
outputFormat: "Weekly operating note with metrics, risks, and next actions.",
|
|
40
39
|
author: SAMPLE_PROFILE_AUTHOR,
|
|
@@ -73,7 +72,6 @@ You review pipeline movement, funnel risk, and rep follow-ups with a bias toward
|
|
|
73
72
|
canUseToolPolicy: {
|
|
74
73
|
autoApprove: ["Read"],
|
|
75
74
|
},
|
|
76
|
-
temperature: 0.6,
|
|
77
75
|
maxTurns: 16,
|
|
78
76
|
outputFormat: "Experiment summary with winning message angles and next tests.",
|
|
79
77
|
author: SAMPLE_PROFILE_AUTHOR,
|
|
@@ -109,7 +107,6 @@ You turn campaign performance and research inputs into sharper launch messaging.
|
|
|
109
107
|
domain: "personal",
|
|
110
108
|
tags: ["investing", "portfolio", "risk", "habits"],
|
|
111
109
|
allowedTools: ["Read", "Write"],
|
|
112
|
-
temperature: 0.25,
|
|
113
110
|
maxTurns: 14,
|
|
114
111
|
outputFormat: "Short investor brief with posture, risk notes, and watchlist changes.",
|
|
115
112
|
author: SAMPLE_PROFILE_AUTHOR,
|
package/src/lib/db/schema.ts
CHANGED
|
@@ -88,6 +88,7 @@ export const notifications = sqliteTable(
|
|
|
88
88
|
"agent_message",
|
|
89
89
|
"budget_alert",
|
|
90
90
|
"context_proposal",
|
|
91
|
+
"context_proposal_batch",
|
|
91
92
|
],
|
|
92
93
|
}).notNull(),
|
|
93
94
|
title: text("title").notNull(),
|
|
@@ -223,6 +224,8 @@ export const usageLedger = sqliteTable(
|
|
|
223
224
|
"scheduled_firing",
|
|
224
225
|
"task_assist",
|
|
225
226
|
"profile_test",
|
|
227
|
+
"pattern_extraction",
|
|
228
|
+
"context_summarization",
|
|
226
229
|
],
|
|
227
230
|
}).notNull(),
|
|
228
231
|
runtimeId: text("runtime_id").notNull(),
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { db } from "@/lib/db";
|
|
2
|
+
import {
|
|
3
|
+
tasks,
|
|
4
|
+
workflows,
|
|
5
|
+
documents,
|
|
6
|
+
schedules,
|
|
7
|
+
usageLedger,
|
|
8
|
+
learnedContext,
|
|
9
|
+
settings,
|
|
10
|
+
} from "@/lib/db/schema";
|
|
11
|
+
import { count, like, sql } from "drizzle-orm";
|
|
12
|
+
import type { AdoptionEntry } from "./types";
|
|
13
|
+
|
|
14
|
+
function toDepth(n: number): AdoptionEntry["depth"] {
|
|
15
|
+
if (n === 0) return "none";
|
|
16
|
+
if (n <= 3) return "light";
|
|
17
|
+
return "deep";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function entry(n: number): AdoptionEntry {
|
|
21
|
+
return { adopted: n > 0, depth: toDepth(n) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Map each manifest section slug to DB evidence of adoption */
|
|
25
|
+
export async function getAdoptionMap(): Promise<Map<string, AdoptionEntry>> {
|
|
26
|
+
const [
|
|
27
|
+
taskCount,
|
|
28
|
+
workflowCount,
|
|
29
|
+
documentCount,
|
|
30
|
+
scheduleCount,
|
|
31
|
+
profileUsed,
|
|
32
|
+
usageCount,
|
|
33
|
+
learnedCount,
|
|
34
|
+
permissionCount,
|
|
35
|
+
providerCount,
|
|
36
|
+
] = await Promise.all([
|
|
37
|
+
db
|
|
38
|
+
.select({ n: count() })
|
|
39
|
+
.from(tasks)
|
|
40
|
+
.then((r) => r[0]?.n ?? 0),
|
|
41
|
+
db
|
|
42
|
+
.select({ n: count() })
|
|
43
|
+
.from(workflows)
|
|
44
|
+
.then((r) => r[0]?.n ?? 0),
|
|
45
|
+
db
|
|
46
|
+
.select({ n: count() })
|
|
47
|
+
.from(documents)
|
|
48
|
+
.then((r) => r[0]?.n ?? 0),
|
|
49
|
+
db
|
|
50
|
+
.select({ n: count() })
|
|
51
|
+
.from(schedules)
|
|
52
|
+
.then((r) => r[0]?.n ?? 0),
|
|
53
|
+
db
|
|
54
|
+
.select({ n: count() })
|
|
55
|
+
.from(tasks)
|
|
56
|
+
.where(sql`${tasks.agentProfile} IS NOT NULL`)
|
|
57
|
+
.then((r) => r[0]?.n ?? 0),
|
|
58
|
+
db
|
|
59
|
+
.select({ n: count() })
|
|
60
|
+
.from(usageLedger)
|
|
61
|
+
.then((r) => r[0]?.n ?? 0),
|
|
62
|
+
db
|
|
63
|
+
.select({ n: count() })
|
|
64
|
+
.from(learnedContext)
|
|
65
|
+
.then((r) => r[0]?.n ?? 0),
|
|
66
|
+
db
|
|
67
|
+
.select({ n: count() })
|
|
68
|
+
.from(settings)
|
|
69
|
+
.where(like(settings.key, "permission:%"))
|
|
70
|
+
.then((r) => r[0]?.n ?? 0),
|
|
71
|
+
db
|
|
72
|
+
.select({ n: sql<number>`COUNT(DISTINCT ${usageLedger.providerId})` })
|
|
73
|
+
.from(usageLedger)
|
|
74
|
+
.then((r) => Number(r[0]?.n ?? 0)),
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
const map = new Map<string, AdoptionEntry>();
|
|
78
|
+
|
|
79
|
+
// Always adopted
|
|
80
|
+
map.set("home-workspace", { adopted: true, depth: "deep" });
|
|
81
|
+
map.set("settings", { adopted: true, depth: "deep" });
|
|
82
|
+
|
|
83
|
+
// Feature sections
|
|
84
|
+
map.set("dashboard-kanban", entry(taskCount));
|
|
85
|
+
map.set("inbox-notifications", entry(taskCount)); // notifications come with tasks
|
|
86
|
+
map.set("monitoring", entry(taskCount));
|
|
87
|
+
map.set("projects", entry(taskCount)); // projects enable tasks
|
|
88
|
+
map.set("workflows", entry(workflowCount));
|
|
89
|
+
map.set("documents", entry(documentCount));
|
|
90
|
+
map.set("profiles", entry(profileUsed));
|
|
91
|
+
map.set("schedules", entry(scheduleCount));
|
|
92
|
+
map.set("cost-usage", entry(usageCount));
|
|
93
|
+
|
|
94
|
+
// Cross-cutting
|
|
95
|
+
map.set(
|
|
96
|
+
"provider-runtimes",
|
|
97
|
+
providerCount > 1
|
|
98
|
+
? { adopted: true, depth: "deep" }
|
|
99
|
+
: entry(usageCount > 0 ? 1 : 0)
|
|
100
|
+
);
|
|
101
|
+
map.set("agent-intelligence", entry(learnedCount));
|
|
102
|
+
map.set("tool-permissions", entry(permissionCount));
|
|
103
|
+
|
|
104
|
+
return map;
|
|
105
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AdoptionEntry, DocJourney, JourneyCompletion } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Compute journey completion from adoption data */
|
|
4
|
+
export function getJourneyCompletions(
|
|
5
|
+
journeys: DocJourney[],
|
|
6
|
+
adoptionMap: Map<string, AdoptionEntry>
|
|
7
|
+
): Map<string, JourneyCompletion> {
|
|
8
|
+
const completions = new Map<string, JourneyCompletion>();
|
|
9
|
+
|
|
10
|
+
for (const journey of journeys) {
|
|
11
|
+
const total = journey.sections.length;
|
|
12
|
+
const completed = journey.sections.filter(
|
|
13
|
+
(slug) => adoptionMap.get(slug)?.adopted === true
|
|
14
|
+
).length;
|
|
15
|
+
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
16
|
+
|
|
17
|
+
completions.set(journey.slug, { completed, total, percentage });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return completions;
|
|
21
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, existsSync } from "fs";
|
|
2
|
+
import { join, basename } from "path";
|
|
3
|
+
import type { DocManifest, ParsedDoc } from "./types";
|
|
4
|
+
|
|
5
|
+
/** Resolve the docs directory relative to project root */
|
|
6
|
+
function docsDir(): string {
|
|
7
|
+
return join(process.cwd(), "docs");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Read and parse docs/manifest.json */
|
|
11
|
+
export function getManifest(): DocManifest {
|
|
12
|
+
const raw = readFileSync(join(docsDir(), "manifest.json"), "utf-8");
|
|
13
|
+
return JSON.parse(raw) as DocManifest;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Parse a markdown file into frontmatter + body */
|
|
17
|
+
function parseMarkdown(content: string, slug: string): ParsedDoc {
|
|
18
|
+
const fmRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
|
|
19
|
+
const match = content.match(fmRegex);
|
|
20
|
+
|
|
21
|
+
if (!match) {
|
|
22
|
+
return { frontmatter: {}, body: content, slug };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const frontmatter: Record<string, unknown> = {};
|
|
26
|
+
const fmLines = match[1].split("\n");
|
|
27
|
+
for (const line of fmLines) {
|
|
28
|
+
const colonIdx = line.indexOf(":");
|
|
29
|
+
if (colonIdx === -1) continue;
|
|
30
|
+
const key = line.slice(0, colonIdx).trim();
|
|
31
|
+
let value: unknown = line.slice(colonIdx + 1).trim();
|
|
32
|
+
|
|
33
|
+
// Strip surrounding quotes
|
|
34
|
+
if (
|
|
35
|
+
typeof value === "string" &&
|
|
36
|
+
value.startsWith('"') &&
|
|
37
|
+
value.endsWith('"')
|
|
38
|
+
) {
|
|
39
|
+
value = value.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Parse arrays: ["a", "b"]
|
|
43
|
+
if (typeof value === "string" && value.startsWith("[")) {
|
|
44
|
+
try {
|
|
45
|
+
value = JSON.parse(value);
|
|
46
|
+
} catch {
|
|
47
|
+
// keep as string
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
frontmatter[key] = value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { frontmatter, body: match[2], slug };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read a single doc by slug (searches features/ then journeys/ then root) */
|
|
58
|
+
export function getDocBySlug(slug: string): ParsedDoc | null {
|
|
59
|
+
const dirs = ["features", "journeys", ""];
|
|
60
|
+
for (const dir of dirs) {
|
|
61
|
+
const filePath = join(docsDir(), dir, `${slug}.md`);
|
|
62
|
+
if (existsSync(filePath)) {
|
|
63
|
+
const content = readFileSync(filePath, "utf-8");
|
|
64
|
+
return parseMarkdown(content, slug);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Read all docs from features/ and journeys/ */
|
|
71
|
+
export function getAllDocs(): ParsedDoc[] {
|
|
72
|
+
const docs: ParsedDoc[] = [];
|
|
73
|
+
const subDirs = ["features", "journeys"];
|
|
74
|
+
|
|
75
|
+
for (const dir of subDirs) {
|
|
76
|
+
const dirPath = join(docsDir(), dir);
|
|
77
|
+
if (!existsSync(dirPath)) continue;
|
|
78
|
+
|
|
79
|
+
const files = readdirSync(dirPath).filter((f) => f.endsWith(".md"));
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
const content = readFileSync(join(dirPath, file), "utf-8");
|
|
82
|
+
const slug = basename(file, ".md");
|
|
83
|
+
docs.push(parseMarkdown(content, slug));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Also check for getting-started.md at root
|
|
88
|
+
const gsPath = join(docsDir(), "getting-started.md");
|
|
89
|
+
if (existsSync(gsPath)) {
|
|
90
|
+
const content = readFileSync(gsPath, "utf-8");
|
|
91
|
+
docs.push(parseMarkdown(content, "getting-started"));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return docs;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Read the .last-generated timestamp */
|
|
98
|
+
export function getDocsLastGenerated(): string | null {
|
|
99
|
+
const filePath = join(docsDir(), ".last-generated");
|
|
100
|
+
if (!existsSync(filePath)) return null;
|
|
101
|
+
return readFileSync(filePath, "utf-8").trim();
|
|
102
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Types for the Playbook documentation system
|
|
2
|
+
|
|
3
|
+
export interface DocManifest {
|
|
4
|
+
generated: string;
|
|
5
|
+
version: number;
|
|
6
|
+
sections: DocSection[];
|
|
7
|
+
journeys: DocJourney[];
|
|
8
|
+
metadata: {
|
|
9
|
+
totalDocs: number;
|
|
10
|
+
totalScreengrabs: number;
|
|
11
|
+
featuresCovered: number;
|
|
12
|
+
appSections: number;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DocSection {
|
|
17
|
+
slug: string;
|
|
18
|
+
title: string;
|
|
19
|
+
category: string;
|
|
20
|
+
path: string;
|
|
21
|
+
route: string;
|
|
22
|
+
tags: string[];
|
|
23
|
+
features: string[];
|
|
24
|
+
screengrabCount: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DocJourney {
|
|
28
|
+
slug: string;
|
|
29
|
+
title: string;
|
|
30
|
+
persona: string;
|
|
31
|
+
difficulty: string;
|
|
32
|
+
path: string;
|
|
33
|
+
sections: string[];
|
|
34
|
+
stepCount: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ParsedDoc {
|
|
38
|
+
frontmatter: Record<string, unknown>;
|
|
39
|
+
body: string;
|
|
40
|
+
slug: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type UsageStage = "new" | "early" | "active" | "power";
|
|
44
|
+
|
|
45
|
+
export interface AdoptionEntry {
|
|
46
|
+
adopted: boolean;
|
|
47
|
+
depth: "none" | "light" | "deep";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface JourneyCompletion {
|
|
51
|
+
completed: number;
|
|
52
|
+
total: number;
|
|
53
|
+
percentage: number;
|
|
54
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { db } from "@/lib/db";
|
|
2
|
+
import {
|
|
3
|
+
tasks,
|
|
4
|
+
projects,
|
|
5
|
+
workflows,
|
|
6
|
+
schedules,
|
|
7
|
+
usageLedger,
|
|
8
|
+
} from "@/lib/db/schema";
|
|
9
|
+
import { count, isNotNull } from "drizzle-orm";
|
|
10
|
+
import type { UsageStage } from "./types";
|
|
11
|
+
|
|
12
|
+
/** Compute the user's usage stage from DB state */
|
|
13
|
+
export async function getUsageStage(): Promise<UsageStage> {
|
|
14
|
+
const [taskCount, projectCount, workflowCount, scheduleCount, profileUsed] =
|
|
15
|
+
await Promise.all([
|
|
16
|
+
db
|
|
17
|
+
.select({ n: count() })
|
|
18
|
+
.from(tasks)
|
|
19
|
+
.then((r) => r[0]?.n ?? 0),
|
|
20
|
+
db
|
|
21
|
+
.select({ n: count() })
|
|
22
|
+
.from(projects)
|
|
23
|
+
.then((r) => r[0]?.n ?? 0),
|
|
24
|
+
db
|
|
25
|
+
.select({ n: count() })
|
|
26
|
+
.from(workflows)
|
|
27
|
+
.then((r) => r[0]?.n ?? 0),
|
|
28
|
+
db
|
|
29
|
+
.select({ n: count() })
|
|
30
|
+
.from(schedules)
|
|
31
|
+
.then((r) => r[0]?.n ?? 0),
|
|
32
|
+
db
|
|
33
|
+
.select({ n: count() })
|
|
34
|
+
.from(tasks)
|
|
35
|
+
.where(isNotNull(tasks.agentProfile))
|
|
36
|
+
.then((r) => r[0]?.n ?? 0),
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// Power: has workflows + schedules + profiles
|
|
40
|
+
if (workflowCount > 0 && scheduleCount > 0 && profileUsed > 0) {
|
|
41
|
+
return "power";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Active: 6+ tasks OR 3+ projects OR any workflows/schedules
|
|
45
|
+
if (
|
|
46
|
+
taskCount >= 6 ||
|
|
47
|
+
projectCount >= 3 ||
|
|
48
|
+
workflowCount > 0 ||
|
|
49
|
+
scheduleCount > 0
|
|
50
|
+
) {
|
|
51
|
+
return "active";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Early: 1-5 tasks, <=2 projects
|
|
55
|
+
if (taskCount >= 1 || projectCount >= 1) {
|
|
56
|
+
return "early";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return "new";
|
|
60
|
+
}
|
|
@@ -80,6 +80,7 @@ export async function listPendingApprovalPayloads(
|
|
|
80
80
|
inArray(notifications.type, [
|
|
81
81
|
"permission_required",
|
|
82
82
|
"context_proposal",
|
|
83
|
+
"context_proposal_batch",
|
|
83
84
|
]),
|
|
84
85
|
isNull(notifications.response)
|
|
85
86
|
)
|
|
@@ -89,6 +90,7 @@ export async function listPendingApprovalPayloads(
|
|
|
89
90
|
|
|
90
91
|
return rows.map((row) => {
|
|
91
92
|
const isContextProposal = row.type === "context_proposal";
|
|
93
|
+
const isBatchProposal = row.type === "context_proposal_batch";
|
|
92
94
|
const parsedInput = parseNotificationToolInput(row.toolInput);
|
|
93
95
|
|
|
94
96
|
return {
|
|
@@ -97,16 +99,22 @@ export async function listPendingApprovalPayloads(
|
|
|
97
99
|
taskId: row.taskId,
|
|
98
100
|
workflowId: row.workflowId,
|
|
99
101
|
toolName: row.toolName,
|
|
100
|
-
permissionLabel:
|
|
101
|
-
? "
|
|
102
|
-
:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
permissionLabel: isBatchProposal
|
|
103
|
+
? "Workflow Learning"
|
|
104
|
+
: isContextProposal
|
|
105
|
+
? "Context Proposal"
|
|
106
|
+
: getPermissionKindLabel(row.toolName),
|
|
107
|
+
compactSummary: isBatchProposal
|
|
108
|
+
? `Batch of learned patterns from workflow execution`
|
|
109
|
+
: isContextProposal
|
|
110
|
+
? `Learned patterns proposed for profile "${row.toolName}"`
|
|
111
|
+
: buildPermissionSummary(row.toolName, parsedInput),
|
|
112
|
+
deepLink: isBatchProposal
|
|
113
|
+
? "/inbox"
|
|
114
|
+
: isContextProposal
|
|
115
|
+
? `/profiles/${row.toolName}`
|
|
116
|
+
: buildDeepLink(row.taskId, row.workflowId),
|
|
117
|
+
supportedActionIds: (isContextProposal || isBatchProposal)
|
|
110
118
|
? (["allow_once", "deny"] as ApprovalActionId[])
|
|
111
119
|
: [...APPROVAL_ACTION_IDS],
|
|
112
120
|
title: row.title,
|