stagent 0.1.0 → 0.1.2
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 +33 -30
- package/dist/cli.js +376 -49
- package/package.json +23 -24
- package/public/desktop-icon-512.png +0 -0
- package/public/icon-512.png +0 -0
- package/src/app/api/data/clear/route.ts +0 -7
- package/src/app/api/data/seed/route.ts +0 -7
- package/src/app/api/profiles/[id]/context/route.ts +109 -0
- package/src/components/dashboard/__tests__/accessibility.test.tsx +42 -0
- package/src/components/documents/__tests__/document-upload-dialog.test.tsx +46 -0
- package/src/components/notifications/__tests__/pending-approval-host.test.tsx +122 -0
- package/src/components/notifications/__tests__/permission-response-actions.test.tsx +79 -0
- package/src/components/notifications/pending-approval-host.tsx +49 -25
- package/src/components/profiles/context-proposal-review.tsx +145 -0
- package/src/components/profiles/learned-context-panel.tsx +286 -0
- package/src/components/profiles/profile-detail-view.tsx +4 -0
- package/src/components/projects/__tests__/dialog-focus.test.tsx +87 -0
- package/src/components/tasks/__tests__/kanban-board-accessibility.test.tsx +59 -0
- package/src/lib/__tests__/setup-verify.test.ts +28 -0
- package/src/lib/__tests__/utils.test.ts +29 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +946 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +63 -0
- package/src/lib/agents/__tests__/router.test.ts +61 -0
- package/src/lib/agents/claude-agent.ts +34 -5
- package/src/lib/agents/learned-context.ts +322 -0
- package/src/lib/agents/pattern-extractor.ts +150 -0
- package/src/lib/agents/profiles/__tests__/compatibility.test.ts +76 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +177 -0
- package/src/lib/agents/profiles/builtins/sweep/SKILL.md +47 -0
- package/src/lib/agents/profiles/builtins/sweep/profile.yaml +12 -0
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +38 -0
- package/src/lib/agents/runtime/openai-codex.ts +1 -1
- package/src/lib/agents/sweep.ts +65 -0
- package/src/lib/constants/__tests__/task-status.test.ts +119 -0
- package/src/lib/data/seed-data/__tests__/profiles.test.ts +141 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +56 -0
- package/src/lib/db/bootstrap.ts +301 -0
- package/src/lib/db/index.ts +2 -205
- package/src/lib/db/migrations/0004_add_documents.sql +2 -1
- package/src/lib/db/migrations/0005_add_document_preprocessing.sql +2 -0
- package/src/lib/db/migrations/0006_add_agent_profile.sql +1 -0
- package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +9 -2
- package/src/lib/db/migrations/meta/_journal.json +43 -1
- package/src/lib/db/schema.ts +34 -0
- package/src/lib/desktop/__tests__/sidecar-launch.test.ts +70 -0
- package/src/lib/desktop/sidecar-launch.ts +85 -0
- package/src/lib/documents/__tests__/context-builder.test.ts +57 -0
- package/src/lib/documents/__tests__/output-scanner.test.ts +141 -0
- package/src/lib/notifications/actionable.ts +21 -7
- package/src/lib/settings/__tests__/auth.test.ts +220 -0
- package/src/lib/settings/__tests__/budget-guardrails.test.ts +181 -0
- package/src/lib/tauri-bridge.ts +138 -0
- package/src/lib/usage/__tests__/ledger.test.ts +284 -0
- package/src/lib/utils/__tests__/crypto.test.ts +90 -0
- package/src/lib/validators/__tests__/profile.test.ts +119 -0
- package/src/lib/validators/__tests__/project.test.ts +82 -0
- package/src/lib/validators/__tests__/settings.test.ts +151 -0
- package/src/lib/validators/__tests__/task.test.ts +144 -0
- package/src/lib/workflows/__tests__/definition-validation.test.ts +164 -0
- package/src/lib/workflows/__tests__/engine.test.ts +114 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/parallel.test.ts +75 -0
- package/src/lib/workflows/__tests__/swarm.test.ts +97 -0
- package/src/test/setup.ts +10 -0
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
> A governed AI agent operations workspace for running, supervising, and reusing AI work through projects, workflows, documents, profiles, schedules, inbox approvals, and live monitoring.
|
|
4
4
|
|
|
5
|
+
[](https://github.com/navam-io/stagent/releases/latest/download/Stagent.dmg) [](https://github.com/navam-io/stagent/releases/latest)
|
|
6
|
+
|
|
5
7
|
[](https://nextjs.org/) [](https://react.dev/) [](https://www.typescriptlang.org/) [](https://docs.anthropic.com/) [](https://developers.openai.com/codex/app-server) [](LICENSE)
|
|
6
8
|
|
|
7
9
|
## Why Stagent
|
|
@@ -10,34 +12,21 @@ AI agents are powerful, but production deployment breaks down when teams cannot
|
|
|
10
12
|
|
|
11
13
|
Stagent is a local-first AI operations workspace built around governed execution and reusable automation primitives. Instead of treating every agent run as a one-off prompt, it gives teams a structured system of home workspace signals, execution dashboards, project context, workflow blueprints, reusable profiles, schedules, documents, inbox approvals, and live monitoring.
|
|
12
14
|
|
|
13
|
-
##
|
|
15
|
+
## Get Started
|
|
14
16
|
|
|
15
|
-
###
|
|
17
|
+
### Download Desktop for macOS
|
|
16
18
|
|
|
17
|
-
|
|
19
|
+
**Primary install path:** [Download the latest macOS desktop release](https://github.com/navam-io/stagent/releases/latest/download/Stagent.dmg)
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
export ANTHROPIC_API_KEY=your-anthropic-key
|
|
21
|
-
export OPENAI_API_KEY=your-openai-key
|
|
21
|
+
Grab the latest `Stagent.dmg` or `Stagent.app.zip` asset from GitHub Releases, then move `Stagent.app` into `/Applications`.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
```
|
|
23
|
+
Current desktop release notes:
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
- macOS-only for now
|
|
26
|
+
- GitHub desktop releases are intended to be Developer ID signed and notarized; local smoke builds created with `npm run desktop:release -- --skip-upload` will still warn if Apple credentials are not configured
|
|
27
|
+
- the current desktop wrapper expects `node` to be available on the machine
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
npx stagent --port 3210
|
|
30
|
-
npx stagent --no-open
|
|
31
|
-
npm install -g stagent && stagent
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Stagent stores local state in `~/.stagent/` by default. To isolate a run or smoke-test the packaged app, override the data directory:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
STAGENT_DATA_DIR=/tmp/stagent-smoke npx stagent --reset --no-open
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
<img src="https://unpkg.com/stagent@latest/public/readme/home-workspace.png" alt="Stagent home workspace" width="1200" />
|
|
29
|
+
<img src="./output/playwright/home-playwright-after.png" alt="Stagent home workspace" width="1200" />
|
|
41
30
|
|
|
42
31
|
### Repository development
|
|
43
32
|
|
|
@@ -228,7 +217,10 @@ File upload with drag-and-drop in task creation. Type-aware content preview for
|
|
|
228
217
|
Configuration hub with provider-aware sections: Claude authentication (API key or OAuth), OpenAI Codex runtime API-key management, tool permissions (saved "Always Allow" patterns with revoke), and data management.
|
|
229
218
|
|
|
230
219
|
#### CLI
|
|
231
|
-
|
|
220
|
+
Stagent still uses a small Node sidecar under the hood. It is built from `bin/cli.ts` into `dist/cli.js`, but it is now an internal desktop bootstrap rather than a user-facing distribution channel.
|
|
221
|
+
|
|
222
|
+
#### Tauri Desktop
|
|
223
|
+
The repo produces macOS desktop artifacts via Tauri. Local development uses `npm run desktop:dev`, local packaging uses `npm run desktop:build`, and release publishing now runs locally via `npm run desktop:release` so the uploaded GitHub assets stay on stable names: `Stagent.dmg` and `Stagent.app.zip`. Published releases must be built with `APPLE_SIGNING_IDENTITY` plus notarization credentials so the downloaded app clears Gatekeeper without the "Apple could not verify" malware warning. Keep `public/desktop-icon-512.png` as the dedicated rounded desktop source icon with transparent corners; `public/icon-512.png` remains the square web/PWA icon. The local release script also stamps the same branded icon onto the mounted DMG volume so Finder shows the corrected Stagent icon during install.
|
|
232
224
|
|
|
233
225
|
#### Database
|
|
234
226
|
SQLite with WAL mode via better-sqlite3 + Drizzle ORM. Eight tables: `projects`, `tasks`, `workflows`, `agent_logs`, `notifications`, `documents`, `schedules`, `settings`. Self-healing bootstrap — tables are created on startup if missing.
|
|
@@ -261,21 +253,32 @@ Responsive sidebar with collapsible icon-only mode, custom Stagent logo, tooltip
|
|
|
261
253
|
```bash
|
|
262
254
|
npm run dev # Next.js dev server (Turbopack)
|
|
263
255
|
npm run build:cli # Build CLI → dist/cli.js
|
|
264
|
-
npm run smoke:npm # Pack tarball and launch the packaged CLI from a temp install
|
|
265
256
|
npm test # Run Vitest
|
|
266
257
|
npm run test:coverage # Coverage report
|
|
267
258
|
```
|
|
268
259
|
|
|
269
|
-
##
|
|
260
|
+
## Desktop Release Checklist
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
npm run desktop:release
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
For a build-only smoke check, run `npm run desktop:release -- --skip-upload`.
|
|
267
|
+
|
|
268
|
+
Before publishing a real desktop release, configure Apple signing once on the release machine:
|
|
270
269
|
|
|
271
270
|
```bash
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
271
|
+
export APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (TEAMID)"
|
|
272
|
+
xcrun notarytool store-credentials stagent-notary \
|
|
273
|
+
--apple-id "you@example.com" \
|
|
274
|
+
--team-id "TEAMID" \
|
|
275
|
+
--password "app-specific-password"
|
|
276
|
+
export APPLE_NOTARY_PROFILE="stagent-notary"
|
|
276
277
|
```
|
|
277
278
|
|
|
278
|
-
|
|
279
|
+
You can use direct credentials instead of a keychain profile by exporting `APPLE_ID`, `APPLE_APP_SPECIFIC_PASSWORD`, and `APPLE_TEAM_ID`.
|
|
280
|
+
|
|
281
|
+
The release script builds locally on macOS, signs the app, notarizes and staples the app bundle and DMG when Apple credentials are configured, verifies the DMG, creates or updates the `desktop-v<package.json version>` GitHub release, uploads `Stagent.dmg` and `Stagent.app.zip`, and refreshes the stable download URL at `https://github.com/navam-io/stagent/releases/latest/download/Stagent.dmg`. Uploads now fail fast unless Developer ID signing and notarization are configured so unsigned artifacts are not published by accident.
|
|
279
282
|
|
|
280
283
|
### Project Structure
|
|
281
284
|
|
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// bin/cli.ts
|
|
4
4
|
import { program } from "commander";
|
|
5
|
-
import { dirname, join as
|
|
5
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
7
|
import {
|
|
8
8
|
mkdirSync,
|
|
9
|
-
existsSync,
|
|
9
|
+
existsSync as existsSync2,
|
|
10
10
|
readFileSync,
|
|
11
11
|
writeFileSync,
|
|
12
12
|
cpSync,
|
|
@@ -18,54 +18,372 @@ import Database from "better-sqlite3";
|
|
|
18
18
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
19
19
|
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
20
20
|
|
|
21
|
+
// src/lib/desktop/sidecar-launch.ts
|
|
22
|
+
import { existsSync } from "fs";
|
|
23
|
+
import { dirname, join } from "path";
|
|
24
|
+
var SIDECAR_LOOPBACK_HOST = "127.0.0.1";
|
|
25
|
+
function wasPortExplicitlyRequested(argv) {
|
|
26
|
+
return argv.some(
|
|
27
|
+
(argument) => argument === "-p" || argument === "--port" || argument.startsWith("--port=")
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
async function resolveSidecarPort({
|
|
31
|
+
argv,
|
|
32
|
+
requestedPort: requestedPort2,
|
|
33
|
+
findAvailablePort: findAvailablePort2
|
|
34
|
+
}) {
|
|
35
|
+
if (wasPortExplicitlyRequested(argv)) {
|
|
36
|
+
return requestedPort2;
|
|
37
|
+
}
|
|
38
|
+
return findAvailablePort2(requestedPort2);
|
|
39
|
+
}
|
|
40
|
+
function findClosestPath(cwd, segments, exists = existsSync) {
|
|
41
|
+
let dir = cwd;
|
|
42
|
+
while (true) {
|
|
43
|
+
const candidate = join(dir, ...segments);
|
|
44
|
+
if (exists(candidate)) {
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
const parent = dirname(dir);
|
|
48
|
+
if (parent === dir) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
dir = parent;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function resolveNextEntrypoint(cwd, exists = existsSync) {
|
|
55
|
+
const nextEntrypoint = findClosestPath(cwd, ["node_modules", "next", "dist", "bin", "next"], exists);
|
|
56
|
+
if (nextEntrypoint) {
|
|
57
|
+
return nextEntrypoint;
|
|
58
|
+
}
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Could not resolve Next.js CLI entrypoint from ${cwd}. Expected node_modules/next/dist/bin/next.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
function buildNextLaunchArgs({
|
|
64
|
+
isPrebuilt,
|
|
65
|
+
port,
|
|
66
|
+
host = SIDECAR_LOOPBACK_HOST
|
|
67
|
+
}) {
|
|
68
|
+
if (isPrebuilt) {
|
|
69
|
+
return ["start", "--hostname", host, "--port", String(port)];
|
|
70
|
+
}
|
|
71
|
+
return ["dev", "--turbopack", "--hostname", host, "--port", String(port)];
|
|
72
|
+
}
|
|
73
|
+
function buildSidecarUrl(port, host = SIDECAR_LOOPBACK_HOST) {
|
|
74
|
+
return `http://${host}:${port}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
21
77
|
// src/lib/utils/stagent-paths.ts
|
|
22
78
|
import { homedir } from "os";
|
|
23
|
-
import { join } from "path";
|
|
79
|
+
import { join as join2 } from "path";
|
|
24
80
|
function getStagentDataDir() {
|
|
25
|
-
return process.env.STAGENT_DATA_DIR ||
|
|
81
|
+
return process.env.STAGENT_DATA_DIR || join2(homedir(), ".stagent");
|
|
26
82
|
}
|
|
27
83
|
function getStagentDbPath() {
|
|
28
|
-
return
|
|
84
|
+
return join2(getStagentDataDir(), "stagent.db");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/lib/db/bootstrap.ts
|
|
88
|
+
import { readMigrationFiles } from "drizzle-orm/migrator";
|
|
89
|
+
var STAGENT_TABLES = [
|
|
90
|
+
"projects",
|
|
91
|
+
"tasks",
|
|
92
|
+
"workflows",
|
|
93
|
+
"agent_logs",
|
|
94
|
+
"notifications",
|
|
95
|
+
"settings",
|
|
96
|
+
"documents",
|
|
97
|
+
"schedules",
|
|
98
|
+
"usage_ledger",
|
|
99
|
+
"learned_context"
|
|
100
|
+
];
|
|
101
|
+
function bootstrapStagentDatabase(sqlite2) {
|
|
102
|
+
sqlite2.exec(`
|
|
103
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
104
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
105
|
+
name TEXT NOT NULL,
|
|
106
|
+
description TEXT,
|
|
107
|
+
status TEXT DEFAULT 'active' NOT NULL,
|
|
108
|
+
created_at INTEGER NOT NULL,
|
|
109
|
+
updated_at INTEGER NOT NULL
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
113
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
114
|
+
project_id TEXT,
|
|
115
|
+
workflow_id TEXT,
|
|
116
|
+
schedule_id TEXT,
|
|
117
|
+
title TEXT NOT NULL,
|
|
118
|
+
description TEXT,
|
|
119
|
+
status TEXT DEFAULT 'planned' NOT NULL,
|
|
120
|
+
assigned_agent TEXT,
|
|
121
|
+
agent_profile TEXT,
|
|
122
|
+
priority INTEGER DEFAULT 2 NOT NULL,
|
|
123
|
+
result TEXT,
|
|
124
|
+
session_id TEXT,
|
|
125
|
+
resume_count INTEGER DEFAULT 0 NOT NULL,
|
|
126
|
+
created_at INTEGER NOT NULL,
|
|
127
|
+
updated_at INTEGER NOT NULL,
|
|
128
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
129
|
+
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
130
|
+
FOREIGN KEY (schedule_id) REFERENCES schedules(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
134
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
135
|
+
project_id TEXT,
|
|
136
|
+
name TEXT NOT NULL,
|
|
137
|
+
definition TEXT NOT NULL,
|
|
138
|
+
status TEXT DEFAULT 'draft' NOT NULL,
|
|
139
|
+
created_at INTEGER NOT NULL,
|
|
140
|
+
updated_at INTEGER NOT NULL,
|
|
141
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
CREATE TABLE IF NOT EXISTS agent_logs (
|
|
145
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
146
|
+
task_id TEXT,
|
|
147
|
+
agent_type TEXT NOT NULL,
|
|
148
|
+
event TEXT NOT NULL,
|
|
149
|
+
payload TEXT,
|
|
150
|
+
timestamp INTEGER NOT NULL,
|
|
151
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
155
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
156
|
+
task_id TEXT,
|
|
157
|
+
type TEXT NOT NULL,
|
|
158
|
+
title TEXT NOT NULL,
|
|
159
|
+
body TEXT,
|
|
160
|
+
read INTEGER DEFAULT 0 NOT NULL,
|
|
161
|
+
tool_name TEXT,
|
|
162
|
+
tool_input TEXT,
|
|
163
|
+
response TEXT,
|
|
164
|
+
responded_at INTEGER,
|
|
165
|
+
created_at INTEGER NOT NULL,
|
|
166
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
170
|
+
key TEXT PRIMARY KEY,
|
|
171
|
+
value TEXT NOT NULL,
|
|
172
|
+
updated_at INTEGER NOT NULL
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
176
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_project_id ON tasks(project_id);
|
|
177
|
+
CREATE INDEX IF NOT EXISTS idx_agent_logs_task_id ON agent_logs(task_id);
|
|
178
|
+
CREATE INDEX IF NOT EXISTS idx_agent_logs_timestamp ON agent_logs(timestamp);
|
|
179
|
+
|
|
180
|
+
CREATE TABLE IF NOT EXISTS documents (
|
|
181
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
182
|
+
task_id TEXT,
|
|
183
|
+
project_id TEXT,
|
|
184
|
+
filename TEXT NOT NULL,
|
|
185
|
+
original_name TEXT NOT NULL,
|
|
186
|
+
mime_type TEXT NOT NULL,
|
|
187
|
+
size INTEGER NOT NULL,
|
|
188
|
+
storage_path TEXT NOT NULL,
|
|
189
|
+
version INTEGER DEFAULT 1 NOT NULL,
|
|
190
|
+
direction TEXT DEFAULT 'input' NOT NULL,
|
|
191
|
+
category TEXT,
|
|
192
|
+
status TEXT DEFAULT 'uploaded' NOT NULL,
|
|
193
|
+
extracted_text TEXT,
|
|
194
|
+
processed_path TEXT,
|
|
195
|
+
processing_error TEXT,
|
|
196
|
+
created_at INTEGER NOT NULL,
|
|
197
|
+
updated_at INTEGER NOT NULL,
|
|
198
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
199
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
CREATE TABLE IF NOT EXISTS schedules (
|
|
203
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
204
|
+
project_id TEXT,
|
|
205
|
+
name TEXT NOT NULL,
|
|
206
|
+
prompt TEXT NOT NULL,
|
|
207
|
+
cron_expression TEXT NOT NULL,
|
|
208
|
+
assigned_agent TEXT,
|
|
209
|
+
agent_profile TEXT,
|
|
210
|
+
recurs INTEGER DEFAULT 1 NOT NULL,
|
|
211
|
+
status TEXT DEFAULT 'active' NOT NULL,
|
|
212
|
+
max_firings INTEGER,
|
|
213
|
+
firing_count INTEGER DEFAULT 0 NOT NULL,
|
|
214
|
+
expires_at INTEGER,
|
|
215
|
+
last_fired_at INTEGER,
|
|
216
|
+
next_fire_at INTEGER,
|
|
217
|
+
created_at INTEGER NOT NULL,
|
|
218
|
+
updated_at INTEGER NOT NULL,
|
|
219
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
CREATE INDEX IF NOT EXISTS idx_schedules_status ON schedules(status);
|
|
223
|
+
CREATE INDEX IF NOT EXISTS idx_schedules_next_fire_at ON schedules(next_fire_at);
|
|
224
|
+
CREATE INDEX IF NOT EXISTS idx_schedules_project_id ON schedules(project_id);
|
|
225
|
+
|
|
226
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_task_id ON notifications(task_id);
|
|
227
|
+
CREATE INDEX IF NOT EXISTS idx_notifications_read ON notifications(read);
|
|
228
|
+
CREATE INDEX IF NOT EXISTS idx_documents_task_id ON documents(task_id);
|
|
229
|
+
CREATE INDEX IF NOT EXISTS idx_documents_project_id ON documents(project_id);
|
|
230
|
+
|
|
231
|
+
CREATE TABLE IF NOT EXISTS usage_ledger (
|
|
232
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
233
|
+
task_id TEXT,
|
|
234
|
+
workflow_id TEXT,
|
|
235
|
+
schedule_id TEXT,
|
|
236
|
+
project_id TEXT,
|
|
237
|
+
activity_type TEXT NOT NULL,
|
|
238
|
+
runtime_id TEXT NOT NULL,
|
|
239
|
+
provider_id TEXT NOT NULL,
|
|
240
|
+
model_id TEXT,
|
|
241
|
+
status TEXT NOT NULL,
|
|
242
|
+
input_tokens INTEGER,
|
|
243
|
+
output_tokens INTEGER,
|
|
244
|
+
total_tokens INTEGER,
|
|
245
|
+
cost_micros INTEGER,
|
|
246
|
+
pricing_version TEXT,
|
|
247
|
+
started_at INTEGER NOT NULL,
|
|
248
|
+
finished_at INTEGER NOT NULL,
|
|
249
|
+
FOREIGN KEY (task_id) REFERENCES tasks(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
250
|
+
FOREIGN KEY (workflow_id) REFERENCES workflows(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
251
|
+
FOREIGN KEY (schedule_id) REFERENCES schedules(id) ON UPDATE NO ACTION ON DELETE NO ACTION,
|
|
252
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
CREATE INDEX IF NOT EXISTS idx_usage_ledger_task_id ON usage_ledger(task_id);
|
|
256
|
+
CREATE INDEX IF NOT EXISTS idx_usage_ledger_activity_type ON usage_ledger(activity_type);
|
|
257
|
+
CREATE INDEX IF NOT EXISTS idx_usage_ledger_runtime_id ON usage_ledger(runtime_id);
|
|
258
|
+
CREATE INDEX IF NOT EXISTS idx_usage_ledger_provider_model ON usage_ledger(provider_id, model_id);
|
|
259
|
+
CREATE INDEX IF NOT EXISTS idx_usage_ledger_finished_at ON usage_ledger(finished_at);
|
|
260
|
+
|
|
261
|
+
CREATE TABLE IF NOT EXISTS learned_context (
|
|
262
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
263
|
+
profile_id TEXT NOT NULL,
|
|
264
|
+
version INTEGER NOT NULL,
|
|
265
|
+
content TEXT,
|
|
266
|
+
diff TEXT,
|
|
267
|
+
change_type TEXT NOT NULL,
|
|
268
|
+
source_task_id TEXT,
|
|
269
|
+
proposal_notification_id TEXT,
|
|
270
|
+
proposed_additions TEXT,
|
|
271
|
+
approved_by TEXT,
|
|
272
|
+
created_at INTEGER NOT NULL,
|
|
273
|
+
FOREIGN KEY (source_task_id) REFERENCES tasks(id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
CREATE INDEX IF NOT EXISTS idx_learned_context_profile_version ON learned_context(profile_id, version);
|
|
277
|
+
CREATE INDEX IF NOT EXISTS idx_learned_context_change_type ON learned_context(change_type);
|
|
278
|
+
`);
|
|
279
|
+
try {
|
|
280
|
+
sqlite2.exec(`ALTER TABLE tasks ADD COLUMN agent_profile TEXT;`);
|
|
281
|
+
} catch {
|
|
282
|
+
}
|
|
283
|
+
sqlite2.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_agent_profile ON tasks(agent_profile);`);
|
|
284
|
+
try {
|
|
285
|
+
sqlite2.exec(`ALTER TABLE tasks ADD COLUMN workflow_id TEXT REFERENCES workflows(id);`);
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
sqlite2.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_workflow_id ON tasks(workflow_id);`);
|
|
289
|
+
try {
|
|
290
|
+
sqlite2.exec(`ALTER TABLE tasks ADD COLUMN schedule_id TEXT REFERENCES schedules(id);`);
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
sqlite2.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_schedule_id ON tasks(schedule_id);`);
|
|
294
|
+
try {
|
|
295
|
+
sqlite2.exec(`ALTER TABLE projects ADD COLUMN working_directory TEXT;`);
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
sqlite2.exec(`ALTER TABLE schedules ADD COLUMN assigned_agent TEXT;`);
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
sqlite2.exec(`ALTER TABLE documents ADD COLUMN version INTEGER NOT NULL DEFAULT 1;`);
|
|
304
|
+
} catch {
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function hasLegacyStagentTables(sqlite2) {
|
|
308
|
+
const placeholders = STAGENT_TABLES.map(() => "?").join(", ");
|
|
309
|
+
const row = sqlite2.prepare(
|
|
310
|
+
`SELECT COUNT(*) AS count
|
|
311
|
+
FROM sqlite_master
|
|
312
|
+
WHERE type = 'table' AND name IN (${placeholders})`
|
|
313
|
+
).get(...STAGENT_TABLES);
|
|
314
|
+
return row.count > 0;
|
|
315
|
+
}
|
|
316
|
+
function hasMigrationHistory(sqlite2, migrationsTable = "__drizzle_migrations") {
|
|
317
|
+
const tableRow = sqlite2.prepare(
|
|
318
|
+
`SELECT COUNT(*) AS count
|
|
319
|
+
FROM sqlite_master
|
|
320
|
+
WHERE type = 'table' AND name = ?`
|
|
321
|
+
).get(migrationsTable);
|
|
322
|
+
if (tableRow.count === 0) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
const row = sqlite2.prepare(`SELECT COUNT(*) AS count FROM ${migrationsTable}`).get();
|
|
326
|
+
return row.count > 0;
|
|
327
|
+
}
|
|
328
|
+
function markAllMigrationsApplied(sqlite2, migrationsFolder, migrationsTable = "__drizzle_migrations") {
|
|
329
|
+
const migrations = readMigrationFiles({ migrationsFolder, migrationsTable });
|
|
330
|
+
sqlite2.exec(`
|
|
331
|
+
CREATE TABLE IF NOT EXISTS ${migrationsTable} (
|
|
332
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
333
|
+
hash TEXT NOT NULL,
|
|
334
|
+
created_at NUMERIC
|
|
335
|
+
)
|
|
336
|
+
`);
|
|
337
|
+
const existing = sqlite2.prepare(`SELECT hash, created_at FROM ${migrationsTable}`).all();
|
|
338
|
+
const seen = new Set(existing.map((row) => `${row.hash}:${row.created_at}`));
|
|
339
|
+
const insert = sqlite2.prepare(
|
|
340
|
+
`INSERT INTO ${migrationsTable} (hash, created_at) VALUES (?, ?)`
|
|
341
|
+
);
|
|
342
|
+
for (const migration of migrations) {
|
|
343
|
+
const key = `${migration.hash}:${migration.folderMillis}`;
|
|
344
|
+
if (!seen.has(key)) {
|
|
345
|
+
insert.run(migration.hash, migration.folderMillis);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
29
348
|
}
|
|
30
349
|
|
|
31
350
|
// bin/cli.ts
|
|
32
|
-
var __dirname =
|
|
33
|
-
var appDir =
|
|
351
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
352
|
+
var appDir = join3(__dirname, "..");
|
|
34
353
|
var DATA_DIR = getStagentDataDir();
|
|
35
354
|
var dbPath = getStagentDbPath();
|
|
36
|
-
var pkg = JSON.parse(readFileSync(
|
|
355
|
+
var pkg = JSON.parse(readFileSync(join3(appDir, "package.json"), "utf-8"));
|
|
37
356
|
var HELP_TEXT = `
|
|
38
357
|
Data:
|
|
39
358
|
Directory ${DATA_DIR}
|
|
40
359
|
Database ${dbPath}
|
|
41
|
-
Sessions ${
|
|
42
|
-
Logs ${
|
|
360
|
+
Sessions ${join3(DATA_DIR, "sessions")}
|
|
361
|
+
Logs ${join3(DATA_DIR, "logs")}
|
|
43
362
|
|
|
44
363
|
Environment variables:
|
|
45
|
-
STAGENT_DATA_DIR Custom data directory for the
|
|
364
|
+
STAGENT_DATA_DIR Custom data directory for the desktop sidecar and web app
|
|
46
365
|
ANTHROPIC_API_KEY Claude runtime access
|
|
47
366
|
OPENAI_API_KEY OpenAI Codex runtime access
|
|
48
367
|
|
|
49
368
|
Examples:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
STAGENT_DATA_DIR=/tmp/stagent-smoke npx stagent --reset
|
|
369
|
+
node dist/cli.js --port 3210 --no-open
|
|
370
|
+
STAGENT_DATA_DIR=/tmp/stagent-desktop node dist/cli.js --reset
|
|
53
371
|
`;
|
|
54
|
-
program.name("stagent").description("Governed AI agent workspace
|
|
372
|
+
program.name("stagent").description("Governed desktop AI agent workspace").version(pkg.version).addHelpText("after", HELP_TEXT).option("-p, --port <number>", "port to start on", "3000").option("--reset", "delete the local database before starting").option("--no-open", "don't auto-open browser").parse();
|
|
55
373
|
var opts = program.opts();
|
|
56
374
|
var requestedPort = Number.parseInt(opts.port, 10);
|
|
57
375
|
if (Number.isNaN(requestedPort) || requestedPort <= 0) {
|
|
58
376
|
program.error(`Invalid port: ${opts.port}`);
|
|
59
377
|
}
|
|
60
|
-
for (const dir of [DATA_DIR,
|
|
378
|
+
for (const dir of [DATA_DIR, join3(DATA_DIR, "logs"), join3(DATA_DIR, "sessions")]) {
|
|
61
379
|
mkdirSync(dir, { recursive: true });
|
|
62
380
|
}
|
|
63
381
|
if (opts.reset) {
|
|
64
|
-
if (
|
|
382
|
+
if (existsSync2(dbPath)) {
|
|
65
383
|
unlinkSync(dbPath);
|
|
66
384
|
for (const suffix of ["-wal", "-shm"]) {
|
|
67
385
|
const filePath = dbPath + suffix;
|
|
68
|
-
if (
|
|
386
|
+
if (existsSync2(filePath)) unlinkSync(filePath);
|
|
69
387
|
}
|
|
70
388
|
console.log("Database reset.");
|
|
71
389
|
} else {
|
|
@@ -75,9 +393,17 @@ if (opts.reset) {
|
|
|
75
393
|
var sqlite = new Database(dbPath);
|
|
76
394
|
sqlite.pragma("journal_mode = WAL");
|
|
77
395
|
sqlite.pragma("foreign_keys = ON");
|
|
396
|
+
var migrationsDir = join3(appDir, "src", "lib", "db", "migrations");
|
|
78
397
|
var db = drizzle(sqlite);
|
|
79
|
-
var
|
|
80
|
-
|
|
398
|
+
var needsLegacyRecovery = hasLegacyStagentTables(sqlite) && !hasMigrationHistory(sqlite);
|
|
399
|
+
if (needsLegacyRecovery) {
|
|
400
|
+
bootstrapStagentDatabase(sqlite);
|
|
401
|
+
markAllMigrationsApplied(sqlite, migrationsDir);
|
|
402
|
+
console.log("Recovered legacy database schema.");
|
|
403
|
+
} else {
|
|
404
|
+
migrate(db, { migrationsFolder: migrationsDir });
|
|
405
|
+
bootstrapStagentDatabase(sqlite);
|
|
406
|
+
}
|
|
81
407
|
sqlite.close();
|
|
82
408
|
console.log("Database ready.");
|
|
83
409
|
function findAvailablePort(preferred) {
|
|
@@ -91,31 +417,24 @@ function findAvailablePort(preferred) {
|
|
|
91
417
|
});
|
|
92
418
|
});
|
|
93
419
|
}
|
|
94
|
-
function findLocalBin(name, cwd) {
|
|
95
|
-
const local = join2(cwd, "node_modules", ".bin", name);
|
|
96
|
-
if (existsSync(local)) return local;
|
|
97
|
-
let dir = dirname(cwd);
|
|
98
|
-
while (dir !== dirname(dir)) {
|
|
99
|
-
const candidate = join2(dir, "node_modules", ".bin", name);
|
|
100
|
-
if (existsSync(candidate)) return candidate;
|
|
101
|
-
dir = dirname(dir);
|
|
102
|
-
}
|
|
103
|
-
return name;
|
|
104
|
-
}
|
|
105
420
|
async function main() {
|
|
106
|
-
const actualPort = await
|
|
421
|
+
const actualPort = await resolveSidecarPort({
|
|
422
|
+
argv: process.argv.slice(2),
|
|
423
|
+
requestedPort,
|
|
424
|
+
findAvailablePort
|
|
425
|
+
});
|
|
107
426
|
let effectiveCwd = appDir;
|
|
108
|
-
const localNm =
|
|
109
|
-
if (!
|
|
110
|
-
let searchDir =
|
|
111
|
-
while (searchDir !==
|
|
112
|
-
const candidate =
|
|
113
|
-
if (
|
|
427
|
+
const localNm = join3(appDir, "node_modules");
|
|
428
|
+
if (!existsSync2(join3(localNm, "next", "package.json"))) {
|
|
429
|
+
let searchDir = dirname2(appDir);
|
|
430
|
+
while (searchDir !== dirname2(searchDir)) {
|
|
431
|
+
const candidate = join3(searchDir, "node_modules", "next", "package.json");
|
|
432
|
+
if (existsSync2(candidate)) {
|
|
114
433
|
const hoistedRoot = searchDir;
|
|
115
434
|
for (const name of ["src", "public"]) {
|
|
116
|
-
const dest =
|
|
117
|
-
const src =
|
|
118
|
-
if (!
|
|
435
|
+
const dest = join3(hoistedRoot, name);
|
|
436
|
+
const src = join3(appDir, name);
|
|
437
|
+
if (!existsSync2(dest) && existsSync2(src)) {
|
|
119
438
|
cpSync(src, dest, { recursive: true });
|
|
120
439
|
}
|
|
121
440
|
}
|
|
@@ -127,23 +446,31 @@ async function main() {
|
|
|
127
446
|
"components.json",
|
|
128
447
|
"drizzle.config.ts"
|
|
129
448
|
]) {
|
|
130
|
-
const dest =
|
|
131
|
-
const src =
|
|
132
|
-
if (!
|
|
449
|
+
const dest = join3(hoistedRoot, name);
|
|
450
|
+
const src = join3(appDir, name);
|
|
451
|
+
if (!existsSync2(dest) && existsSync2(src)) {
|
|
133
452
|
writeFileSync(dest, readFileSync(src));
|
|
134
453
|
}
|
|
135
454
|
}
|
|
136
455
|
effectiveCwd = hoistedRoot;
|
|
137
456
|
break;
|
|
138
457
|
}
|
|
139
|
-
searchDir =
|
|
458
|
+
searchDir = dirname2(searchDir);
|
|
140
459
|
}
|
|
141
460
|
}
|
|
142
|
-
const
|
|
461
|
+
const nextEntrypoint = resolveNextEntrypoint(effectiveCwd);
|
|
462
|
+
const isPrebuilt = existsSync2(join3(effectiveCwd, ".next", "BUILD_ID"));
|
|
463
|
+
const nextArgs = buildNextLaunchArgs({
|
|
464
|
+
isPrebuilt,
|
|
465
|
+
port: actualPort
|
|
466
|
+
});
|
|
467
|
+
const sidecarUrl = buildSidecarUrl(actualPort);
|
|
143
468
|
console.log(`Stagent ${pkg.version}`);
|
|
144
469
|
console.log(`Data dir: ${DATA_DIR}`);
|
|
145
|
-
console.log(`
|
|
146
|
-
|
|
470
|
+
console.log(`Mode: ${isPrebuilt ? "production" : "development"}`);
|
|
471
|
+
console.log(`Next entry: ${nextEntrypoint}`);
|
|
472
|
+
console.log(`Starting Stagent on ${sidecarUrl}`);
|
|
473
|
+
const child = spawn(process.execPath, [nextEntrypoint, ...nextArgs], {
|
|
147
474
|
cwd: effectiveCwd,
|
|
148
475
|
stdio: "inherit",
|
|
149
476
|
env: {
|
|
@@ -156,7 +483,7 @@ async function main() {
|
|
|
156
483
|
setTimeout(async () => {
|
|
157
484
|
try {
|
|
158
485
|
const open = (await import("open")).default;
|
|
159
|
-
await open(
|
|
486
|
+
await open(sidecarUrl);
|
|
160
487
|
} catch {
|
|
161
488
|
}
|
|
162
489
|
}, 3e3);
|