@feltdb/core 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +25 -0
  2. package/dist/application-development.d.ts +78 -0
  3. package/dist/application-development.d.ts.map +1 -0
  4. package/dist/application-development.js +153 -0
  5. package/dist/application-manifest.d.ts +6 -1
  6. package/dist/application-manifest.d.ts.map +1 -1
  7. package/dist/application-manifest.js +10 -3
  8. package/dist/canonical-application.d.ts +27 -0
  9. package/dist/canonical-application.d.ts.map +1 -0
  10. package/dist/canonical-application.js +46 -0
  11. package/dist/cli/ai-model-config.js +97 -0
  12. package/dist/cli/cli.js +1 -1
  13. package/dist/cli/commands.js +664 -120
  14. package/dist/cli/index.js +1 -1
  15. package/dist/cli/managed-environment.js +6 -0
  16. package/dist/cli/managed-project.js +6 -0
  17. package/dist/cli/proposal-client.js +63 -0
  18. package/dist/cli/publish-engine.js +165 -0
  19. package/dist/cli/source-sync.js +183 -0
  20. package/dist/create/cli.js +11 -2
  21. package/dist/create/create.js +38 -16
  22. package/dist/create/managed-account.js +82 -11
  23. package/dist/create/package-versions.js +1 -1
  24. package/dist/create/server-source/crates/feltdb/src/application.rs +41 -1
  25. package/dist/create/server-source/crates/feltdb/src/authority_failover.rs +84 -6
  26. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +126 -38
  27. package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +72 -34
  28. package/dist/create/server-source/crates/feltdb/src/lib.rs +20 -2
  29. package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +12 -24
  30. package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +11 -3
  31. package/dist/create/server-source/crates/feltdb-server/src/authenticated_principal.rs +0 -1
  32. package/dist/create/server-source/crates/feltdb-server/src/certification_harness.rs +23 -22
  33. package/dist/create/server-source/crates/feltdb-server/src/delegation_token.rs +5 -15
  34. package/dist/create/server-source/crates/feltdb-server/src/durable_operations.rs +13 -28
  35. package/dist/create/server-source/crates/feltdb-server/src/identity.rs +111 -3
  36. package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +28 -7
  37. package/dist/create/server-source/crates/feltdb-server/src/lib.rs +5 -5
  38. package/dist/create/server-source/crates/feltdb-server/src/main.rs +1960 -143
  39. package/dist/create/server-source/crates/feltdb-server/src/managed_diagnostics.rs +10 -30
  40. package/dist/create/server-source/crates/feltdb-server/src/membership_policy.rs +12 -4
  41. package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +8 -6
  42. package/dist/create/server-source/crates/feltdb-server/src/snapshot_cursor.rs +10 -14
  43. package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +195 -13
  44. package/dist/create/server-source/crates/feltdb-server/src/tenant_policies.rs +9 -21
  45. package/dist/create/server-source/crates/feltdb-server/src/transaction_idempotency.rs +5 -3
  46. package/dist/create/server-source/crates/feltdb-server/src/transaction_recovery.rs +9 -8
  47. package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +1 -4
  48. package/dist/create/template/default-project/README.md +57 -0
  49. package/dist/create/template/default-project/agents/activity-assistant.ts +18 -0
  50. package/dist/create/template/default-project/agents/project-assistant.ts +19 -0
  51. package/dist/create/template/default-project/capabilities/activity-summary.ts +14 -0
  52. package/dist/create/template/default-project/capabilities/project-search.ts +16 -0
  53. package/dist/create/template/default-project/capabilities/project-summary.ts +15 -0
  54. package/dist/create/template/default-project/feltdb.flow +174 -0
  55. package/dist/create/template/default-project/src/App.tsx +20 -0
  56. package/dist/create/template/default-project/src/context/AuthContext.tsx +27 -0
  57. package/dist/create/template/default-project/src/feltdb.ts +87 -0
  58. package/dist/create/template/default-project/src/index.tsx +14 -0
  59. package/dist/create/template/default-project/src/pages/Activity.tsx +7 -0
  60. package/dist/create/template/default-project/src/pages/Agents.tsx +15 -0
  61. package/dist/create/template/default-project/src/pages/Dashboard.tsx +17 -0
  62. package/dist/create/template/default-project/src/pages/Invitations.tsx +11 -0
  63. package/dist/create/template/default-project/src/pages/Projects.tsx +11 -0
  64. package/dist/create/template/default-project/src/pages/SignIn.tsx +8 -0
  65. package/dist/create/template/default-project/src/pages/SignUp.tsx +8 -0
  66. package/dist/create/template/default-project/src/styles-application.css +21 -0
  67. package/dist/create/template/default-project/src/styles.css +1 -0
  68. package/dist/create/template/default-project/workflows/agent-assisted-summary.ts +1 -0
  69. package/dist/create/template/default-project/workflows/invitation.ts +30 -0
  70. package/dist/create/template/default-project/workflows/project-created.ts +2 -0
  71. package/dist/db.d.ts +49 -1
  72. package/dist/db.d.ts.map +1 -1
  73. package/dist/db.js +77 -0
  74. package/dist/error-codes.d.ts +20 -1
  75. package/dist/error-codes.d.ts.map +1 -1
  76. package/dist/error-codes.js +25 -0
  77. package/dist/flowspec.d.ts +1 -0
  78. package/dist/flowspec.d.ts.map +1 -1
  79. package/dist/flowspec.js +66 -11
  80. package/dist/http-client.d.ts +135 -14
  81. package/dist/http-client.d.ts.map +1 -1
  82. package/dist/http-client.js +79 -31
  83. package/dist/http-db.d.ts +15 -2
  84. package/dist/http-db.d.ts.map +1 -1
  85. package/dist/http-db.js +113 -5
  86. package/dist/index-core.d.ts +5 -0
  87. package/dist/index-core.d.ts.map +1 -1
  88. package/dist/index-core.js +5 -0
  89. package/dist/module-intent.d.ts +37 -0
  90. package/dist/module-intent.d.ts.map +1 -0
  91. package/dist/module-intent.js +81 -0
  92. package/dist/module.d.ts +76 -0
  93. package/dist/module.d.ts.map +1 -0
  94. package/dist/module.js +61 -0
  95. package/dist/proposal.d.ts +161 -0
  96. package/dist/proposal.d.ts.map +1 -0
  97. package/dist/proposal.js +232 -0
  98. package/dist/studio/app.d.ts +5 -1
  99. package/dist/studio/app.d.ts.map +1 -1
  100. package/dist/studio/components/ApplicationDesigner.d.ts +2 -1
  101. package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
  102. package/dist/studio/components/AskFeltDB.d.ts +21 -0
  103. package/dist/studio/components/AskFeltDB.d.ts.map +1 -0
  104. package/dist/studio/components/ContractInspector.d.ts +7 -0
  105. package/dist/studio/components/ContractInspector.d.ts.map +1 -0
  106. package/dist/studio/components/ManagedInstancePanel.d.ts.map +1 -1
  107. package/dist/studio/components/ManagedInstancePanel.js +1 -5
  108. package/dist/studio/components/ModuleExplorer.d.ts +6 -0
  109. package/dist/studio/components/ModuleExplorer.d.ts.map +1 -0
  110. package/dist/studio/components/ProposalBrowser.d.ts +12 -0
  111. package/dist/studio/components/ProposalBrowser.d.ts.map +1 -0
  112. package/dist/studio/components/StateExplorer.d.ts +6 -7
  113. package/dist/studio/components/StateExplorer.d.ts.map +1 -1
  114. package/dist/studio/components/index.d.ts +4 -0
  115. package/dist/studio/components/index.d.ts.map +1 -1
  116. package/dist/studio/components/index.js +4 -4
  117. package/dist/studio/components-CNSNrmA9.js +2787 -0
  118. package/dist/studio/index.js +214 -157
  119. package/dist/studio/studio.css +1 -1
  120. package/dist/studio/utils/index.d.ts +2 -0
  121. package/dist/studio/utils/index.d.ts.map +1 -1
  122. package/dist/studio/utils/index.js +14 -12
  123. package/dist/studio/utils/managed-instance.d.ts +0 -4
  124. package/dist/studio/utils/managed-instance.d.ts.map +1 -1
  125. package/dist/studio/utils/managed-instance.js +0 -4
  126. package/dist/studio/utils/proposal-api.d.ts +53 -0
  127. package/dist/studio/utils/proposal-api.d.ts.map +1 -0
  128. package/dist/studio/utils/proposal-api.js +91 -0
  129. package/dist/studio/utils/service-api.d.ts +81 -0
  130. package/dist/studio/utils/service-api.d.ts.map +1 -0
  131. package/dist/studio/utils/service-api.js +51 -0
  132. package/dist/studio-app/assets/dist-CCOk39Uc.js +1 -0
  133. package/dist/studio-app/assets/{feltdb_wasm-DVKsw75S.js → feltdb_wasm-CSB6KVgw.js} +1 -1
  134. package/dist/studio-app/assets/feltdb_wasm_bg-Dd1fnO9U.wasm +0 -0
  135. package/dist/studio-app/assets/index-Bncji3aN.js +43 -0
  136. package/dist/studio-app/assets/{index-C1GWyazR.css → index-yazaWMUN.css} +1 -1
  137. package/dist/studio-app/assets/lib-B2_7fcn7.js +69 -0
  138. package/dist/studio-app/assets/worker-CARZ5g7K.js +69 -0
  139. package/dist/studio-app/index.html +2 -2
  140. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  141. package/dist/workspace/local-development-authority.d.ts +3 -0
  142. package/dist/workspace/local-development-authority.d.ts.map +1 -1
  143. package/dist/workspace/local-development-authority.js +124 -2
  144. package/package.json +1 -1
  145. package/dist/studio/components-Dxuhrv8_.js +0 -1832
  146. package/dist/studio-app/assets/feltdb_wasm_bg-DNyNf0yy.wasm +0 -0
  147. package/dist/studio-app/assets/index-B5tnmvSD.js +0 -28
@@ -0,0 +1,57 @@
1
+ # My FeltDB App
2
+
3
+ This small application demonstrates FeltDB's complete programming model:
4
+
5
+ - Authentication and authorization
6
+ - Multi-tenant application state
7
+ - Atomic workflows
8
+ - Agents with bounded capabilities
9
+ - Reactive UI through `@feltdb/core/react`
10
+ - Shared inspection through FeltDB Studio
11
+
12
+ ## Run it
13
+
14
+ ```bash
15
+ npm install
16
+ npm run feltdb:dev
17
+ ```
18
+
19
+ In a second terminal:
20
+
21
+ ```bash
22
+ npm run dev
23
+ ```
24
+
25
+ Then sign up, create a project, invite an existing user, and run an assistant. Open the same state in Studio:
26
+
27
+ ## Choose an AI model
28
+
29
+ `npm run feltdb:ai` uses private browser-local WebLLM by default. Use
30
+ `feltdb ai model set --provider local-webllm --model <model-id>` for a larger
31
+ local model, or configure an OpenAI-compatible model with a credential supplied
32
+ through `--api-key-stdin`. `feltdb ai model show` displays the active choice.
33
+ Hosted credentials remain in the operating-system vault and are never exposed
34
+ to the application or Studio.
35
+
36
+ ```bash
37
+ npm run feltdb:studio
38
+ ```
39
+
40
+ ## How the example fits together
41
+
42
+ - `feltdb.flow` defines state, authorization, workflows, capabilities, and agents.
43
+ - `workflows/` implements project creation, invitation lifecycle, and agent-assisted summary examples.
44
+ - `capabilities/` contains the narrow read and summary operations available to assistants.
45
+ - `agents/` registers the Project and Activity assistants and persists their execution lifecycle.
46
+ - `src/` contains the authenticated React application.
47
+
48
+ These folders make the concepts easy to find; they are not mandatory application architecture. Agents and workflows use the same FeltDB collections as the UI, so they do not bypass `authenticated`, `member`, `owner`, or `self(user)` policies.
49
+
50
+ For managed FeltDB, generate with `--runtime managed`; the model and application remain the same while the runtime configuration changes.
51
+
52
+ Preview and publish the same definition to its configured cloud environment:
53
+
54
+ ```bash
55
+ npm run feltdb:publish -- --dry-run
56
+ npm run feltdb:publish -- --yes
57
+ ```
@@ -0,0 +1,18 @@
1
+ import { db, recordAgentActivity } from '../src/feltdb';
2
+ import { activitySummary, type ActivitySummary } from '../capabilities/activity-summary';
3
+
4
+ export const activityAssistant = db.defineAgent({ name: 'activity-assistant', version: 1, description: 'Explains authorized application activity.', capabilities: [activitySummary.id], goals: ['Summarize recent authorized audit events'], constraints: { maxIterations: 1 } });
5
+
6
+ export async function runActivityAssistant(input: { organization: string; user: string }): Promise<ActivitySummary> {
7
+ const execution = await db.createAgentExecution(activityAssistant, 'Summarize recent activity', []);
8
+ try {
9
+ await db.startAgentExecution(execution);
10
+ const result = await activitySummary.execute() as ActivitySummary;
11
+ await recordAgentActivity({ organization: input.organization, user: input.user, assistant: 'Activity Assistant', subject: execution.executionId });
12
+ await db.completeAgentExecution(execution, execution.executionId);
13
+ return result;
14
+ } catch (error) {
15
+ await db.failAgentExecution(execution, error instanceof Error ? error.message : String(error));
16
+ throw error;
17
+ }
18
+ }
@@ -0,0 +1,19 @@
1
+ import { db, recordAgentActivity } from '../src/feltdb';
2
+ import { projectSearch } from '../capabilities/project-search';
3
+ import { projectSummary, type ProjectSummary } from '../capabilities/project-summary';
4
+
5
+ export const projectAssistant = db.defineAgent({ name: 'project-assistant', version: 1, description: 'Summarizes authorized project state.', capabilities: [projectSearch.id, projectSummary.id], goals: ['Explain project state without widening the actor authorization boundary'], constraints: { maxIterations: 1 } });
6
+
7
+ export async function runProjectAssistant(input: { projectId: string; organization: string; user: string }): Promise<ProjectSummary> {
8
+ const execution = await db.createAgentExecution(projectAssistant, 'Summarize the selected project', [input.projectId]);
9
+ try {
10
+ await db.startAgentExecution(execution);
11
+ const result = await projectSummary.execute(input.projectId) as ProjectSummary;
12
+ await recordAgentActivity({ organization: input.organization, user: input.user, assistant: 'Project Assistant', subject: input.projectId });
13
+ await db.completeAgentExecution(execution, input.projectId);
14
+ return result;
15
+ } catch (error) {
16
+ await db.failAgentExecution(execution, error instanceof Error ? error.message : String(error));
17
+ throw error;
18
+ }
19
+ }
@@ -0,0 +1,14 @@
1
+ import type { Capability } from '@feltdb/core';
2
+ import { auditEvents, type AuditEvent } from '../src/feltdb';
3
+
4
+ export interface ActivitySummary { summary: string; total: number; actions: string[] }
5
+
6
+ export const activitySummary: Capability = {
7
+ id: 'activity-summary',
8
+ name: 'Activity summary',
9
+ async execute(): Promise<ActivitySummary> {
10
+ const events = (await auditEvents.all() as AuditEvent[]).slice(-20);
11
+ const actions = [...new Set(events.map(event => event.action))];
12
+ return { total: events.length, actions, summary: events.length ? `${events.length} recent events across ${actions.length} action types: ${actions.join(', ')}.` : 'No application activity has been recorded yet.' };
13
+ },
14
+ };
@@ -0,0 +1,16 @@
1
+ import type { Capability } from '@feltdb/core';
2
+ import { projects, type Project } from '../src/feltdb';
3
+
4
+ export interface ProjectSearchResult { id: string; name: string; description: string }
5
+
6
+ export const projectSearch: Capability = {
7
+ id: 'project-search',
8
+ name: 'Project search',
9
+ async execute(query: string): Promise<ProjectSearchResult[]> {
10
+ // The collection read crosses FeltDB's normal authorization boundary. The
11
+ // capability narrows results; it never grants access to additional rows.
12
+ const words = query.trim().toLowerCase();
13
+ const authorized = await projects.all() as Project[];
14
+ return authorized.filter(project => `${project.name} ${project.description}`.toLowerCase().includes(words)).map(({ id, name, description }) => ({ id, name, description }));
15
+ },
16
+ };
@@ -0,0 +1,15 @@
1
+ import type { Capability } from '@feltdb/core';
2
+ import { auditEvents, projects, type AuditEvent, type Project } from '../src/feltdb';
3
+
4
+ export interface ProjectSummary { title: string; summary: string; activityCount: number }
5
+
6
+ export const projectSummary: Capability = {
7
+ id: 'project-summary',
8
+ name: 'Project summary',
9
+ async execute(projectId: string): Promise<ProjectSummary> {
10
+ const project = await projects.get(projectId) as Project | null;
11
+ if (!project) throw new Error('Project is unavailable in the current authorization context.');
12
+ const activity = (await auditEvents.all() as AuditEvent[]).filter(event => event.resource_id === project.id);
13
+ return { title: project.name, summary: project.description || `${project.name} is active and ready for its next state transition.`, activityCount: activity.length };
14
+ },
15
+ };
@@ -0,0 +1,174 @@
1
+ flow_version 1
2
+
3
+ app FeltDBStarter {
4
+ // Optional billing semantics. Uncomment to include the Stripe-compatible module:
5
+ // module stripe {
6
+ // provider stripe-compatible
7
+ // version 1.0.0
8
+ // }
9
+ collection User {
10
+ email: text
11
+ name: text
12
+ created_at: datetime
13
+ index email using hash(email)
14
+ }
15
+ collection Organization {
16
+ name: text
17
+ created_at: datetime
18
+ }
19
+ collection Membership {
20
+ user: ref User
21
+ organization: ref Organization
22
+ role: text
23
+ created_at: datetime
24
+ index user using hash(user)
25
+ index organization using hash(organization)
26
+ }
27
+ collection Session {
28
+ user: ref User
29
+ expires_at: datetime
30
+ created_at: datetime
31
+ index user using hash(user)
32
+ }
33
+ collection Project {
34
+ organization: ref Organization
35
+ name: text
36
+ description: text
37
+ owner_id: text
38
+ created_at: datetime
39
+ updated_at: datetime
40
+ index organization using hash(organization)
41
+ }
42
+ collection Invitation {
43
+ organization: ref Organization
44
+ user: ref User
45
+ email: text
46
+ role: text
47
+ status: text
48
+ created_at: datetime
49
+ updated_at: datetime
50
+ index organization using hash(organization)
51
+ }
52
+ collection AuditEvent {
53
+ organization: ref Organization
54
+ user: ref User
55
+ action: text
56
+ resource_type: text
57
+ resource_id: text
58
+ details: text
59
+ created_at: datetime
60
+ index organization using hash(organization)
61
+ }
62
+ collection Team {
63
+ organization: ref Organization
64
+ name: text
65
+ description: text
66
+ owner: ref User
67
+ created_at: datetime
68
+ updated_at: datetime
69
+ index organization using hash(organization)
70
+ }
71
+ collection TeamMembership {
72
+ organization: ref Organization
73
+ team: ref Team
74
+ user: ref User
75
+ role: text
76
+ created_at: datetime
77
+ updated_at: datetime
78
+ index organization using hash(organization)
79
+ }
80
+
81
+ capability ProjectSearch {
82
+ visibility public
83
+ read Project
84
+ }
85
+ capability ProjectSummary {
86
+ visibility agent
87
+ read Project
88
+ read AuditEvent
89
+ }
90
+ capability ActivitySummary {
91
+ visibility agent
92
+ read AuditEvent
93
+ }
94
+ capability RecordAgentActivity {
95
+ visibility internal
96
+ write AuditEvent
97
+ }
98
+
99
+ workflow ProjectCreated {
100
+ step persist_project {
101
+ write Project
102
+ }
103
+ step record_activity {
104
+ write AuditEvent
105
+ }
106
+ }
107
+ workflow InvitationLifecycle {
108
+ step create_invitation {
109
+ write Invitation
110
+ }
111
+ step accept_invitation {
112
+ write Membership
113
+ }
114
+ step record_activity {
115
+ write AuditEvent
116
+ }
117
+ }
118
+ workflow AgentAssistedSummary {
119
+ step read_authorized_state {
120
+ capability ProjectSummary
121
+ }
122
+ step record_activity {
123
+ capability RecordAgentActivity
124
+ }
125
+ }
126
+
127
+ agent ProjectAssistant {
128
+ capability ProjectSearch
129
+ capability ProjectSummary
130
+ capability RecordAgentActivity
131
+ workflow AgentAssistedSummary
132
+ }
133
+ agent ActivityAssistant {
134
+ capability ActivitySummary
135
+ capability RecordAgentActivity
136
+ workflow AgentAssistedSummary
137
+ }
138
+ policy User {
139
+ read: authenticated
140
+ write: owner
141
+ }
142
+ policy Organization {
143
+ read: member
144
+ write: owner
145
+ }
146
+ policy Membership {
147
+ read: member
148
+ write: member
149
+ }
150
+ policy Session {
151
+ read: self(user)
152
+ write: self(user)
153
+ }
154
+ policy Project {
155
+ read: member
156
+ write: member
157
+ }
158
+ policy Invitation {
159
+ read: self(user) | member
160
+ write: member
161
+ }
162
+ policy AuditEvent {
163
+ read: member
164
+ write: authenticated
165
+ }
166
+ policy Team {
167
+ read: member
168
+ write: member
169
+ }
170
+ policy TeamMembership {
171
+ read: member
172
+ write: member
173
+ }
174
+ }
@@ -0,0 +1,20 @@
1
+ import { AuthProvider, useAuth } from './context/AuthContext';
2
+ import { Dashboard } from './pages/Dashboard';
3
+ import { SignIn } from './pages/SignIn';
4
+ import { SignUp } from './pages/SignUp';
5
+ import './styles.css';
6
+ import './styles-application.css';
7
+
8
+ function Application() {
9
+ const { authenticated, loading } = useAuth();
10
+ if (loading) return <main className="center"><div className="loader" /><p>Resolving your FeltDB session…</p></main>;
11
+ if (!authenticated) return <AuthScreen />;
12
+ return <Dashboard />;
13
+ }
14
+
15
+ function AuthScreen() {
16
+ const signIn = new URLSearchParams(window.location.search).get('view') === 'signin';
17
+ return <main className="auth-layout"><section className="intro"><a className="brand" href="https://feltdb.com">FeltDB</a><div><span className="eyebrow">State first</span><h1>A real application, from the first command.</h1><p>Sign up to create a durable user, organization, membership, and session—then inspect all of it in Studio.</p></div><div className="flow"><span>Identity</span><i>→</i><span>Tenant</span><i>→</i><span>State</span></div></section><section className="auth-card">{signIn ? <SignIn /> : <SignUp />}</section></main>;
18
+ }
19
+
20
+ export function App() { return <AuthProvider><Application /></AuthProvider>; }
@@ -0,0 +1,27 @@
1
+ import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
2
+ import { useCollection } from '@feltdb/core/react';
3
+ import { createAccount, createSession, db, memberships, organizations, sessions, users, type Membership, type Organization, type Session, type User } from '../feltdb';
4
+
5
+ const SESSION_KEY = 'feltdb.example.session';
6
+ interface AuthValue { loading: boolean; authenticated: boolean; user?: User; session?: Session; organization?: Organization; membership?: Membership; signUp(input: { name: string; email: string; password: string; organization: string }): Promise<void>; signIn(email: string, password: string): Promise<void>; signOut(): Promise<void> }
7
+ const AuthContext = createContext<AuthValue | undefined>(undefined);
8
+
9
+ export function AuthProvider({ children }: { children: ReactNode }) {
10
+ const sessionRecords = useCollection<Session>(sessions); const userRecords = useCollection<User>(users); const membershipRecords = useCollection<Membership>(memberships); const organizationRecords = useCollection<Organization>(organizations);
11
+ const [sessionId, setSessionId] = useState<string | null>(null);
12
+ useEffect(() => setSessionId(sessionStorage.getItem(SESSION_KEY)), []);
13
+ const session = sessionRecords.data.find(item => item.id === sessionId && Date.parse(item.expires_at) > Date.now());
14
+ const user = userRecords.data.find(item => item.id === session?.user);
15
+ const membership = membershipRecords.data.find(item => item.user === user?.id);
16
+ const organization = organizationRecords.data.find(item => item.id === membership?.organization);
17
+ const loading = sessionRecords.loading || userRecords.loading || membershipRecords.loading || organizationRecords.loading;
18
+ const establish = (next: Session) => { sessionStorage.setItem(SESSION_KEY, next.id); setSessionId(next.id); };
19
+ const value = useMemo<AuthValue>(() => ({ loading, authenticated: Boolean(session && user && membership && organization), session, user, membership, organization,
20
+ signUp: async input => { await db.auth.signUp({ email: input.email, password: input.password, display_name: input.name }); establish(await createAccount(input)); },
21
+ signIn: async (email, password) => { await db.auth.signIn({ email, password }); establish(await createSession(email)); },
22
+ signOut: async () => { if (session) await sessions.delete(session.id); await db.auth.signOut(); sessionStorage.removeItem(SESSION_KEY); setSessionId(null); },
23
+ }), [loading, session, user, membership, organization]);
24
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
25
+ }
26
+
27
+ export function useAuth() { const value = useContext(AuthContext); if (!value) throw new Error('useAuth must be used inside AuthProvider'); return value; }
@@ -0,0 +1,87 @@
1
+ import { configureDevelopmentRuntimeBridge, createFeltDB } from '@feltdb/core';
2
+
3
+ configureDevelopmentRuntimeBridge({
4
+ sessionId: import.meta.env.VITE_FELTDB_DEV_SESSION_ID,
5
+ workspaceId: import.meta.env.VITE_FELTDB_WORKSPACE_ID,
6
+ namespace: import.meta.env.VITE_FELTDB_NAMESPACE,
7
+ runtime: import.meta.env.VITE_FELTDB_RUNTIME,
8
+ authorityUrl: import.meta.env.VITE_FELTDB_AUTHORITY_URL,
9
+ bridgeUrl: import.meta.env.VITE_FELTDB_DEV_BRIDGE_URL,
10
+ applicationUrl: import.meta.env.VITE_FELTDB_APPLICATION_URL,
11
+ });
12
+
13
+ export const managedRuntime = Boolean(import.meta.env.VITE_FELTDB_MANAGED_URL);
14
+ export const db = createFeltDB(managedRuntime ? {
15
+ namespace: import.meta.env.VITE_FELTDB_MANAGED_NAMESPACE || 'feltdb-starter',
16
+ server: {
17
+ url: import.meta.env.VITE_FELTDB_MANAGED_URL,
18
+ applicationId: import.meta.env.VITE_FELTDB_MANAGED_APPLICATION_ID,
19
+ environment: import.meta.env.VITE_FELTDB_MANAGED_ENVIRONMENT || 'production',
20
+ },
21
+ } : { namespace: import.meta.env.VITE_FELTDB_NAMESPACE || 'feltdb-starter', browser: true });
22
+
23
+ export interface User { id: string; email: string; name: string; created_at: string }
24
+ export interface Organization { id: string; name: string; created_at: string }
25
+ export interface Membership { id: string; user: string; organization: string; role: 'owner' | 'member' | 'viewer'; created_at: string }
26
+ export interface Session { id: string; user: string; expires_at: string; created_at: string }
27
+ export interface Project { id: string; organization: string; name: string; description: string; owner_id: string; created_at: string; updated_at: string }
28
+ export interface Invitation { id: string; organization: string; user: string; email: string; role: string; status: string; created_at: string; updated_at: string }
29
+ export interface AuditEvent { id: string; organization: string; user: string; action: string; resource_type: string; resource_id: string; details: string; created_at: string }
30
+ export interface Team { id: string; organization: string; name: string; description: string; owner: string; created_at: string; updated_at: string }
31
+ export interface TeamMembership { id: string; organization: string; team: string; user: string; role: string; created_at: string; updated_at: string }
32
+
33
+ export const users = db.collection<User>('User');
34
+ export const organizations = db.collection<Organization>('Organization');
35
+ export const memberships = db.collection<Membership>('Membership');
36
+ export const sessions = db.collection<Session>('Session');
37
+ export const projects = db.collection<Project>('Project');
38
+ export const invitations = db.collection<Invitation>('Invitation');
39
+ export const auditEvents = db.collection<AuditEvent>('AuditEvent');
40
+ export const teams = db.collection<Team>('Team');
41
+ export const teamMemberships = db.collection<TeamMembership>('TeamMembership');
42
+
43
+ const makeId = (prefix: string) => `${prefix}_${crypto.randomUUID()}`;
44
+
45
+ export async function createAccount(input: { name: string; email: string; organization: string }) {
46
+ const email = input.email.trim().toLowerCase();
47
+ if ((await users.find({ email })).length) throw new Error('An account already exists for that email.');
48
+ const now = new Date().toISOString();
49
+ const user: User = { id: makeId('user'), email, name: input.name.trim(), created_at: now };
50
+ const organization: Organization = { id: makeId('org'), name: input.organization.trim(), created_at: now };
51
+ const membership: Membership = { id: makeId('membership'), user: user.id, organization: organization.id, role: 'owner', created_at: now };
52
+ const session: Session = { id: makeId('session'), user: user.id, created_at: now, expires_at: new Date(Date.now() + 86_400_000).toISOString() };
53
+ const event: AuditEvent = { id: makeId('event'), organization: organization.id, user: user.id, action: 'account.created', resource_type: 'User', resource_id: user.id, details: 'Initial organization and owner membership created', created_at: now };
54
+ await db.transaction(async transaction => {
55
+ transaction.collection<User>('User').insert(user, user.id, { requireAbsent: true });
56
+ transaction.collection<Organization>('Organization').insert(organization, organization.id, { requireAbsent: true });
57
+ transaction.collection<Membership>('Membership').insert(membership, membership.id, { requireAbsent: true });
58
+ transaction.collection<Session>('Session').insert(session, session.id, { requireAbsent: true });
59
+ transaction.collection<AuditEvent>('AuditEvent').insert(event, event.id, { requireAbsent: true });
60
+ }, { transactionId: makeId('signup') });
61
+ return session;
62
+ }
63
+
64
+ export async function createSession(emailInput: string) {
65
+ const user = (await users.find({ email: emailInput.trim().toLowerCase() }))[0];
66
+ if (!user) throw new Error('Account not found.');
67
+ const now = new Date().toISOString();
68
+ const session: Session = { id: makeId('session'), user: user.id, created_at: now, expires_at: new Date(Date.now() + 86_400_000).toISOString() };
69
+ await sessions.insert(session, session.id);
70
+ return session;
71
+ }
72
+
73
+ export async function createProject(input: { organization: string; owner: string; name: string }) {
74
+ const now = new Date().toISOString();
75
+ const project: Project = { id: makeId('project'), organization: input.organization, owner_id: input.owner, name: input.name.trim(), description: '', created_at: now, updated_at: now };
76
+ const event: AuditEvent = { id: makeId('event'), organization: input.organization, user: input.owner, action: 'project.created', resource_type: 'Project', resource_id: project.id, details: project.name, created_at: now };
77
+ await db.transaction(async transaction => {
78
+ transaction.collection<Project>('Project').insert(project, project.id, { requireAbsent: true });
79
+ transaction.collection<AuditEvent>('AuditEvent').insert(event, event.id, { requireAbsent: true });
80
+ });
81
+ }
82
+
83
+ export async function recordAgentActivity(input: { organization: string; user: string; assistant: string; subject: string }) {
84
+ const event: AuditEvent = { id: makeId('event'), organization: input.organization, user: input.user, action: 'agent.summary', resource_type: 'AgentExecution', resource_id: input.subject, details: `${input.assistant} summarized authorized application state`, created_at: new Date().toISOString() };
85
+ await auditEvents.insert(event, event.id);
86
+ return event;
87
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Application entry point
3
+ */
4
+
5
+ import React from 'react';
6
+ import ReactDOM from 'react-dom/client';
7
+ import { App } from './App';
8
+
9
+ const root = ReactDOM.createRoot(document.getElementById('root')!);
10
+ root.render(
11
+ <React.StrictMode>
12
+ <App />
13
+ </React.StrictMode>
14
+ );
@@ -0,0 +1,7 @@
1
+ import { useCollection } from '@feltdb/core/react';
2
+ import { auditEvents } from '../feltdb';
3
+
4
+ export function Activity() {
5
+ const state = useCollection(auditEvents);
6
+ return <section className="page"><div className="page-heading"><div><span className="eyebrow">Durable history</span><h1>Activity</h1><p>Workflow and agent actions land in the same application state.</p></div><span className="count">{state.data.length}</span></div><article className="panel records">{state.data.length ? state.data.slice().reverse().map(event => <div className="record" key={event.id}><span className="record-mark">↗</span><div><strong>{event.action}</strong><small>{event.details} · {new Date(event.created_at).toLocaleString()}</small></div></div>) : <div className="empty"><strong>No activity yet</strong><span>Create a project or run an assistant.</span></div>}</article></section>;
7
+ }
@@ -0,0 +1,15 @@
1
+ import { useMemo, useState } from 'react';
2
+ import type { Capability } from '@feltdb/core';
3
+ import { useCapability, useCollection } from '@feltdb/core/react';
4
+ import { projects } from '../feltdb';
5
+ import { useAuth } from '../context/AuthContext';
6
+ import { runProjectAssistant } from '../../agents/project-assistant';
7
+ import { runActivityAssistant } from '../../agents/activity-assistant';
8
+
9
+ export function Agents() {
10
+ const { user, organization } = useAuth(); const projectState = useCollection(projects); const [projectId, setProjectId] = useState('');
11
+ const projectCapability = useMemo<Capability>(() => ({ id: 'run-project-assistant', name: 'Run Project Assistant', execute: () => runProjectAssistant({ projectId, organization: organization!.id, user: user!.id }) }), [projectId, organization, user]);
12
+ const activityCapability = useMemo<Capability>(() => ({ id: 'run-activity-assistant', name: 'Run Activity Assistant', execute: () => runActivityAssistant({ organization: organization!.id, user: user!.id }) }), [organization, user]);
13
+ const projectAgent = useCapability<[], { title: string; summary: string; activityCount: number }>(projectCapability); const activityAgent = useCapability<[], { summary: string; total: number }>(activityCapability);
14
+ return <section className="page"><div className="page-heading"><div><span className="eyebrow">Authorized agents</span><h1>Assistants</h1><p>Each assistant invokes bounded capabilities over the state you can already access.</p></div></div><div className="agent-grid"><article className="panel"><span className="agent-state">Project Assistant · project-summary</span><h2>Explain a project</h2><select value={projectId} onChange={event => setProjectId(event.target.value)}><option value="">Select a project</option>{projectState.data.map(project => <option key={project.id} value={project.id}>{project.name}</option>)}</select><button className="primary" disabled={!projectId || projectAgent.loading} onClick={() => void projectAgent.execute()}>{projectAgent.loading ? 'Running…' : 'Summarize project'}</button>{projectAgent.result && <div className="result"><strong>{projectAgent.result.title}</strong><p>{projectAgent.result.summary}</p><small>{projectAgent.result.activityCount} related activity events</small></div>}</article><article className="panel"><span className="agent-state">Activity Assistant · activity-summary</span><h2>Explain recent activity</h2><p className="muted">Reads authorized AuditEvent state and records its own execution.</p><button className="primary" disabled={activityAgent.loading} onClick={() => void activityAgent.execute()}>{activityAgent.loading ? 'Running…' : 'Summarize activity'}</button>{activityAgent.result && <div className="result"><p>{activityAgent.result.summary}</p></div>}</article></div></section>;
15
+ }
@@ -0,0 +1,17 @@
1
+ import { useState } from 'react';
2
+ import { useCollection } from '@feltdb/core/react';
3
+ import { auditEvents, invitations, projects, teams } from '../feltdb';
4
+ import { useAuth } from '../context/AuthContext';
5
+ import { Projects } from './Projects';
6
+ import { Invitations } from './Invitations';
7
+ import { Activity } from './Activity';
8
+ import { Agents } from './Agents';
9
+
10
+ type View = 'overview' | 'projects' | 'invitations' | 'activity' | 'agents';
11
+
12
+ export function Dashboard() {
13
+ const { user, organization, signOut } = useAuth(); const [view, setView] = useState<View>('overview');
14
+ const projectState = useCollection(projects); const teamState = useCollection(teams); const invitationState = useCollection(invitations); const activityState = useCollection(auditEvents);
15
+ const stats = [{ label: 'Projects', value: projectState.data.length }, { label: 'Teams', value: teamState.data.length }, { label: 'Invitations', value: invitationState.data.filter(item => item.status === 'pending').length }, { label: 'Activity', value: activityState.data.length }];
16
+ return <main className="dashboard"><header><a className="brand" href="https://feltdb.com">FeltDB</a><nav>{(['overview', 'projects', 'invitations', 'activity', 'agents'] as View[]).map(item => <button className={view === item ? 'nav-active' : 'quiet'} onClick={() => setView(item)} key={item}>{item}</button>)}</nav><div><span>{user?.name}</span><button className="quiet" onClick={() => void signOut()}>Sign out</button></div></header>{view === 'overview' && <><section className="hero"><div><span className="eyebrow">{organization?.name}</span><h1>Welcome, {user?.name.split(' ')[0]}.</h1><p>State, workflows, and agents share one authorization boundary.</p></div><code>member · {organization?.id.slice(0, 12)}…</code></section><section className="stats">{stats.map(stat => <article key={stat.label}><span>{stat.label}</span><strong>{stat.value}</strong></article>)}</section><section className="grid"><article className="panel"><span className="eyebrow">Start here</span><h2>One application, visible in Studio</h2><div className="journey"><button onClick={() => setView('projects')}>1 <span>Create a project<small>Atomic Project + AuditEvent workflow</small></span></button><button onClick={() => setView('invitations')}>2 <span>Invite a member<small>Identity and relational authorization</small></span></button><button onClick={() => setView('agents')}>3 <span>Run an assistant<small>Bounded capabilities over authorized state</small></span></button></div></article><aside className="panel"><span className="eyebrow">Runtime</span><h2>One state boundary</h2><div className="stack"><div><i className="online" /><span>Application connected</span></div><div><i /><span>Authorization in FeltDB</span></div><div><i /><span>Workflows and agents persisted</span></div></div><p className="hint">Run <code>npm run feltdb:studio</code> to inspect the same records.</p></aside></section></>}{view === 'projects' && <Projects />}{view === 'invitations' && <Invitations />}{view === 'activity' && <Activity />}{view === 'agents' && <Agents />}</main>;
17
+ }
@@ -0,0 +1,11 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { useCollection } from '@feltdb/core/react';
3
+ import { invitations } from '../feltdb';
4
+ import { useAuth } from '../context/AuthContext';
5
+ import { runAcceptInvitationWorkflow, runInviteMemberWorkflow } from '../../workflows/invitation';
6
+
7
+ export function Invitations() {
8
+ const { user, organization } = useAuth(); const state = useCollection(invitations); const [message, setMessage] = useState('');
9
+ async function invite(event: FormEvent<HTMLFormElement>) { event.preventDefault(); if (!user || !organization) return; const form = event.currentTarget; setMessage(''); try { await runInviteMemberWorkflow({ organization: organization.id, actor: user.id, email: String(new FormData(form).get('email')), role: 'member' }); form.reset(); } catch (error) { setMessage(error instanceof Error ? error.message : 'Invitation failed.'); } }
10
+ return <section className="page"><div className="page-heading"><div><span className="eyebrow">Invitation workflow</span><h1>Invitations</h1><p>Invitation visibility combines <code>self(user) | member</code>.</p></div><span className="count">{state.data.filter(item => item.status === 'pending').length}</span></div><article className="panel"><form className="project-form" onSubmit={invite}><input name="email" type="email" required placeholder="Existing user email" aria-label="Invite email" /><button>Invite member</button></form>{message && <p className="error">{message}</p>}<div className="records">{state.data.length ? state.data.slice().reverse().map(invitation => <div className="record spread" key={invitation.id}><div><strong>{invitation.email}</strong><small>{invitation.role} · {invitation.status}</small></div>{invitation.user === user?.id && invitation.status === 'pending' && <button onClick={() => void runAcceptInvitationWorkflow(invitation, user.id)}>Accept</button>}</div>) : <div className="empty"><strong>No invitations yet</strong><span>For this starter, invite another user who has already signed up.</span></div>}</div></article></section>;
11
+ }
@@ -0,0 +1,11 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { useCollection } from '@feltdb/core/react';
3
+ import { projects } from '../feltdb';
4
+ import { useAuth } from '../context/AuthContext';
5
+ import { runProjectCreatedWorkflow } from '../../workflows/project-created';
6
+
7
+ export function Projects() {
8
+ const { user, organization } = useAuth(); const state = useCollection(projects); const [busy, setBusy] = useState(false);
9
+ async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); if (!user || !organization) return; const form = event.currentTarget; setBusy(true); await runProjectCreatedWorkflow({ organization: organization.id, owner: user.id, name: String(new FormData(form).get('name')) }); form.reset(); setBusy(false); }
10
+ return <section className="page"><div className="page-heading"><div><span className="eyebrow">Project workflow</span><h1>Projects</h1><p>Creating a project atomically persists both Project and AuditEvent state.</p></div><span className="count">{state.data.length}</span></div><article className="panel"><form className="project-form" onSubmit={submit}><input name="name" required placeholder="A new project" aria-label="Project name" /><button disabled={busy}>{busy ? 'Running workflow…' : 'Create project'}</button></form><div className="records">{state.data.length ? state.data.slice().reverse().map(project => <div className="record" key={project.id}><span className="record-mark">{project.name.charAt(0).toUpperCase()}</span><div><strong>{project.name}</strong><small>ProjectCreated workflow · {new Date(project.created_at).toLocaleDateString()}</small></div></div>) : <div className="empty"><strong>No projects yet</strong><span>Create one to produce your first workflow state.</span></div>}</div></article></section>;
11
+ }
@@ -0,0 +1,8 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { useAuth } from '../context/AuthContext';
3
+
4
+ export function SignIn() {
5
+ const { signIn } = useAuth(); const [error, setError] = useState(''); const [busy, setBusy] = useState(false);
6
+ async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); setBusy(true); setError(''); const form = new FormData(event.currentTarget); try { await signIn(String(form.get('email')), String(form.get('password'))); } catch { setError('Those credentials could not be verified.'); setBusy(false); } }
7
+ return <><span className="eyebrow">Welcome back</span><h2>Sign in to FeltDB.</h2><p className="muted">Managed apps exchange credentials for a short-lived, application-scoped actor session.</p><form onSubmit={submit}><label>Email<input name="email" type="email" autoComplete="email" required placeholder="ada@example.com" /></label><label>Password<input name="password" type="password" autoComplete="current-password" required /></label>{error && <p className="error">{error}</p>}<button disabled={busy}>{busy ? 'Signing in…' : 'Sign in'}</button></form><p className="switch">New to this app? <a href="?view=signup">Create an account</a></p></>;
8
+ }
@@ -0,0 +1,8 @@
1
+ import { useState, type FormEvent } from 'react';
2
+ import { useAuth } from '../context/AuthContext';
3
+
4
+ export function SignUp() {
5
+ const { signUp } = useAuth(); const [error, setError] = useState(''); const [busy, setBusy] = useState(false);
6
+ async function submit(event: FormEvent<HTMLFormElement>) { event.preventDefault(); setBusy(true); setError(''); const form = new FormData(event.currentTarget); try { await signUp({ name: String(form.get('name')), email: String(form.get('email')), password: String(form.get('password')), organization: String(form.get('organization')) }); } catch { setError('We could not create your account. Check the details and try again.'); setBusy(false); } }
7
+ return <><span className="eyebrow">Create your workspace</span><h2>Start with durable state.</h2><p className="muted">Your actor, account, and organization are established at the FeltDB boundary.</p><form onSubmit={submit}><label>Name<input name="name" autoComplete="name" required placeholder="Ada Lovelace" /></label><label>Work email<input name="email" type="email" autoComplete="email" required placeholder="ada@example.com" /></label><label>Password<input name="password" type="password" autoComplete="new-password" minLength={8} required /></label><label>Organization<input name="organization" required placeholder="Analytical Engines" /></label>{error && <p className="error">{error}</p>}<button disabled={busy}>{busy ? 'Creating…' : 'Create account'}</button></form><p className="switch">Already have an account? <a href="?view=signin">Sign in</a></p></>;
8
+ }
@@ -0,0 +1,21 @@
1
+ .dashboard header nav { display: flex; gap: .15rem; }
2
+ .dashboard header nav button { text-transform: capitalize; }
3
+ .nav-active { background: #dff0e5; color: #17633b; border-radius: 6px; padding: .5rem .7rem; }
4
+ .page { padding: clamp(3rem, 7vw, 6rem) 0; }
5
+ .page-heading { display: flex; justify-content: space-between; align-items: end; margin-bottom: 2rem; }
6
+ .page-heading h1 { font: 500 clamp(3rem, 6vw, 5rem)/1 Georgia, serif; letter-spacing: -.055em; margin: .6rem 0; }
7
+ .page-heading p { color: #68746d; }
8
+ .count { font: 500 3rem Georgia, serif; color: #268453; }
9
+ .journey { display: grid; margin-top: 1.5rem; }
10
+ .journey button { display: flex; gap: 1rem; text-align: left; background: transparent; border-top: 1px solid #dedfd9; padding: 1.1rem .2rem; color: #268453; font-weight: 800; }
11
+ .journey button span { display: grid; gap: .25rem; color: #102019; }
12
+ .journey small { font-weight: 400; }
13
+ .spread { justify-content: space-between; }
14
+ .spread button { background: #dff0e5; color: #17633b; border-radius: 6px; padding: .55rem .8rem; }
15
+ .agent-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; }
16
+ .agent-grid select { width: 100%; padding: .85rem; border: 1px solid #ccd2cd; border-radius: 8px; background: #fff; margin: 1rem 0; }
17
+ .primary { display: block; width: 100%; padding: .85rem; border-radius: 8px; background: #102019; color: #fff; font-weight: 700; }
18
+ .agent-state { font: 700 .68rem ui-monospace, monospace; color: #268453; text-transform: uppercase; }
19
+ .result { margin-top: 1rem; padding: 1rem; border-radius: 8px; background: #eef3ee; line-height: 1.5; }
20
+ @media (max-width: 900px) { .dashboard header nav { display: none; } }
21
+ @media (max-width: 800px) { .agent-grid { grid-template-columns: 1fr; } }
@@ -0,0 +1 @@
1
+ :root{font-family:Inter,ui-sans-serif,system-ui,sans-serif;color:#102019;background:#f4f3ed;font-synthesis:none}*{box-sizing:border-box}body{margin:0}a{color:inherit}button,input{font:inherit}button{border:0;cursor:pointer}.brand{font-weight:800;font-size:1.2rem;text-decoration:none;letter-spacing:-.04em}.auth-layout{min-height:100vh;display:grid;grid-template-columns:minmax(0,1.25fr) minmax(380px,.75fr)}.intro{padding:clamp(2rem,6vw,6rem);background:#102019;color:#f8f7f1;display:flex;flex-direction:column;justify-content:space-between}.intro h1{max-width:730px;font:500 clamp(3rem,6vw,6.5rem)/.98 Georgia,serif;letter-spacing:-.055em;margin:.8rem 0 1.5rem}.intro p{max-width:590px;color:#bdc9c1;font-size:1.08rem;line-height:1.65}.flow{display:flex;gap:1rem;color:#9eb0a5;font:600 .73rem ui-monospace,monospace;text-transform:uppercase;letter-spacing:.12em}.flow i{color:#49c17d}.auth-card{background:#fbfaf5;padding:clamp(2rem,6vw,6rem);display:flex;flex-direction:column;justify-content:center}h2{font:500 2rem/1.1 Georgia,serif;letter-spacing:-.035em;margin:.7rem 0}.muted,.switch{color:#68746d;line-height:1.55}form{display:grid;gap:1rem;margin:2rem 0 1.5rem}label{display:grid;gap:.45rem;font-weight:650;font-size:.8rem}input{width:100%;border:1px solid #ccd2cd;border-radius:8px;padding:.85rem 1rem;background:#fff;color:#102019}input:focus{outline:2px solid #49c17d;outline-offset:1px}form button,.project-form button{border-radius:8px;padding:.9rem 1.1rem;background:#102019;color:#fff;font-weight:700}button:disabled{opacity:.55}.eyebrow{color:#268453;font:700 .7rem ui-monospace,monospace;letter-spacing:.13em;text-transform:uppercase}.intro .eyebrow{color:#64dd98}.error{color:#a42c2c;font-size:.85rem}.switch{font-size:.85rem}.center{min-height:100vh;display:grid;place-content:center;text-align:center;color:#68746d}.loader{width:24px;height:24px;margin:auto;border:2px solid #cbd2cc;border-top-color:#268453;border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.dashboard{min-height:100vh;padding:0 clamp(1.25rem,5vw,5rem) 5rem}.dashboard header{height:80px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #d5d7d1}.dashboard header>div{display:flex;gap:1.2rem;align-items:center;font-size:.85rem}.quiet{background:transparent;color:#68746d;padding:.5rem}.hero{padding:clamp(3rem,7vw,6.5rem) 0 3rem;display:flex;justify-content:space-between;align-items:end;gap:2rem}.hero h1{font:500 clamp(3rem,7vw,6rem)/1 Georgia,serif;letter-spacing:-.055em;margin:.7rem 0 1rem}.hero p{color:#68746d}code{font:.72rem ui-monospace,monospace;color:#26784e}.stats{display:grid;grid-template-columns:repeat(4,1fr);border:1px solid #d5d7d1;border-radius:12px;overflow:hidden;background:#faf9f4}.stats article{padding:1.3rem;border-right:1px solid #d5d7d1;display:grid;gap:.45rem}.stats article:last-child{border:0}.stats span{color:#68746d;font-size:.75rem}.stats strong{font:500 2rem Georgia,serif}.grid{display:grid;grid-template-columns:1.6fr .8fr;gap:1rem;margin-top:1rem}.panel{background:#faf9f4;border:1px solid #d5d7d1;border-radius:12px;padding:1.5rem}.panel-title{display:flex;align-items:start;justify-content:space-between}.panel-title>span{color:#68746d;font-size:.75rem}.project-form{grid-template-columns:1fr auto;margin:1.4rem 0}.records{display:grid;gap:.6rem}.record{padding:.8rem;border-top:1px solid #e1e1db;display:flex;align-items:center;gap:.8rem}.record-mark{width:36px;height:36px;border-radius:8px;display:grid;place-items:center;background:#dff0e5;color:#26784e;font-weight:800}.record div{display:grid;gap:.2rem}small{color:#7b857f}.empty{min-height:110px;display:grid;place-content:center;text-align:center;gap:.35rem;color:#68746d}.empty strong{color:#102019}.stack{display:grid;gap:1rem;margin:2rem 0}.stack div{display:flex;gap:.7rem;font-size:.85rem}.stack i{width:8px;height:8px;margin-top:.3rem;border-radius:50%;background:#9aa49d}.stack .online{background:#39ad6b;box-shadow:0 0 0 4px #dff0e5}.hint{color:#68746d;font-size:.85rem;line-height:1.6}@media(max-width:800px){.auth-layout,.grid{grid-template-columns:1fr}.intro{min-height:52vh;gap:3rem}.stats{grid-template-columns:repeat(2,1fr)}.stats article:nth-child(2){border-right:0}.hero{align-items:start;flex-direction:column}}
@@ -0,0 +1 @@
1
+ export { runProjectAssistant as runAgentAssistedSummaryWorkflow } from '../agents/project-assistant';