getprismo 0.1.32 → 0.1.34
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 +77 -7
- package/lib/prismo-dev/agent.js +399 -0
- package/lib/prismo-dev/cli.js +868 -0
- package/lib/prismo-dev/cloud-sync.js +492 -0
- package/lib/prismo-dev/guard.js +426 -0
- package/lib/prismo-dev/help.js +520 -0
- package/lib/prismo-dev/instructions.js +179 -0
- package/lib/prismo-dev/receipt.js +79 -1
- package/lib/prismo-dev-scan.js +168 -1017
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,18 +30,21 @@ prismodev covers the full AI coding session:
|
|
|
30
30
|
|
|
31
31
|
```
|
|
32
32
|
before you code npx getprismo doctor
|
|
33
|
-
while you code npx getprismo watch
|
|
33
|
+
while you code npx getprismo guard --watch
|
|
34
34
|
noisy commands npx getprismo shield -- npm test
|
|
35
35
|
after you code npx getprismo receipt
|
|
36
36
|
postmortem npx getprismo replay
|
|
37
|
+
workspace agent npx getprismo agent --watch
|
|
37
38
|
agent-native npx getprismo mcp
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
**doctor** diagnoses the repo, applies safe fixes, and shows the before/after score.
|
|
41
|
-
**
|
|
42
|
-
**
|
|
42
|
+
**guard** runs live guardrails, context throttle, rescue prompts, context firewall, and dashboard-ready prevention events.
|
|
43
|
+
**watch** monitors context pressure live and is the lower-level diagnostic view behind guard.
|
|
44
|
+
**receipt** explains what repeated, what output dominated, what artifacts leaked, what likely influenced the run, and a heuristic context-efficiency score.
|
|
43
45
|
**replay** reconstructs why a session went sideways and prints a recovery prompt.
|
|
44
46
|
**shield** runs noisy commands without dumping full output back into the agent context.
|
|
47
|
+
**agent** connects Prismo Cloud to your local repo so dashboard actions can safely run on this machine.
|
|
45
48
|
**mcp** exposes PrismoDev as local tools so compatible agents can scan, search shield output, and request scoped context directly.
|
|
46
49
|
|
|
47
50
|
---
|
|
@@ -269,9 +272,54 @@ this is intentionally not magic interception yet. it is a safe local-first primi
|
|
|
269
272
|
|
|
270
273
|
---
|
|
271
274
|
|
|
275
|
+
## workspace agent
|
|
276
|
+
|
|
277
|
+
Prismo Cloud can guide the work from the dashboard, but your repo still lives on your machine. `agent` is the local bridge.
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
npx getprismo connect --token <your Prismo API key>
|
|
281
|
+
npx getprismo agent --watch
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
After that, the Prismo workspace can queue safe actions like `doctor`, `sync`, `guard`, `context`, `optimize`, and allowlisted `shield` commands. The local agent claims those actions, executes them in the selected repo, and reports the status back to Prismo Cloud.
|
|
285
|
+
|
|
286
|
+
This keeps the product flow simple:
|
|
287
|
+
|
|
288
|
+
```text
|
|
289
|
+
dashboard recommends fix -> local agent runs safe command -> dashboard refreshes with the result
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`agent` does not upload prompts, source code, file contents, stdout, stderr, or full command logs. It uploads action status and safe aggregate metrics. Cloud actions are intentionally limited; arbitrary shell commands and shell metacharacters are rejected.
|
|
293
|
+
|
|
294
|
+
For CI-style polling or debugging, run one pass:
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
npx getprismo agent --once
|
|
298
|
+
npx getprismo agent --once --json
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
272
303
|
## new: live guardrails mode
|
|
273
304
|
|
|
274
|
-
the easiest proactive mode is:
|
|
305
|
+
the easiest proactive mode is guard:
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
npx getprismo connect --token <your Prismo API key>
|
|
309
|
+
npx getprismo guard --watch
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`guard` packages the local prevention loop: live guardrails, context throttling, context firewall updates, guard event history, and dashboard-ready prevention events. it never uploads prompts, source code, file contents, stdout, stderr, or full command logs.
|
|
313
|
+
|
|
314
|
+
run it once for a snapshot:
|
|
315
|
+
|
|
316
|
+
```bash
|
|
317
|
+
npx getprismo guard
|
|
318
|
+
npx getprismo guard --json
|
|
319
|
+
npx getprismo guard --no-sync
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
the lower-level watch mode is:
|
|
275
323
|
|
|
276
324
|
```bash
|
|
277
325
|
npx getprismo watch --auto
|
|
@@ -600,7 +648,7 @@ npx getprismo receipt
|
|
|
600
648
|
npx getprismo receipt codex --json
|
|
601
649
|
```
|
|
602
650
|
|
|
603
|
-
it summarizes repeated reads, generated artifacts, tool-output floods, repeated commands, likely influence, and the next scoped action to take.
|
|
651
|
+
it summarizes repeated reads, generated artifacts, tool-output floods, repeated commands, likely influence, and the next scoped action to take. it also reports a heuristic context-efficiency metric: decision/progress signals per 1k tokens, with drag factors such as repeated reads, artifact leaks, tool-output floods, and command loops.
|
|
604
652
|
|
|
605
653
|
`replay` is the postmortem view:
|
|
606
654
|
|
|
@@ -623,9 +671,10 @@ it surfaces recurring waste patterns such as the same lockfile leaking into many
|
|
|
623
671
|
```bash
|
|
624
672
|
npx getprismo instructions audit
|
|
625
673
|
npx getprismo instructions ablate --dry-run
|
|
674
|
+
npx getprismo instructions apply --dry-run
|
|
626
675
|
```
|
|
627
676
|
|
|
628
|
-
it scores rules in `CLAUDE.md`, `AGENTS.md`, `.codex/AGENTS.md`, `.codex/instructions.md`, and `.openai/instructions.md`, then separates observable violations, partial compliance, duplicated rules, trim candidates, and influence-unknown rules. `instructions ablate --dry-run` creates a conservative ablation plan with candidates, sample-count guidance, rollback notes, and variance warnings; it does not edit files.
|
|
677
|
+
it scores rules in `CLAUDE.md`, `AGENTS.md`, `.codex/AGENTS.md`, `.codex/instructions.md`, and `.openai/instructions.md`, then separates observable violations, partial compliance, duplicated rules, trim candidates, and influence-unknown rules. `instructions ablate --dry-run` creates a conservative ablation plan with candidates, sample-count guidance, rollback notes, and variance warnings; it does not edit files. `instructions apply` safely removes exact duplicate instruction lines only, writes backups first, and leaves uncertain rules as recommendations.
|
|
629
678
|
|
|
630
679
|
`boundaries` checks parallel-agent isolation:
|
|
631
680
|
|
|
@@ -717,11 +766,12 @@ no install needed. npx runs it directly.
|
|
|
717
766
|
| `cc` | claude code cost breakdown |
|
|
718
767
|
| `cc timeline` | session reconstruction with events |
|
|
719
768
|
| `cursor` | cursor session tracking and ai authorship |
|
|
720
|
-
| `receipt` | run receipt for reads, repeats, output, artifacts, likely influence, and next-run scope |
|
|
769
|
+
| `receipt` | run receipt for reads, repeats, output, artifacts, context efficiency, likely influence, and next-run scope |
|
|
721
770
|
| `replay` | incident replay with root cause and recovery prompt |
|
|
722
771
|
| `timeline` | recurring context-waste patterns across recent sessions |
|
|
723
772
|
| `instructions audit` | instruction ROI audit for CLAUDE.md / AGENTS.md violations, partial compliance, duplicates, and influence-unknown rules |
|
|
724
773
|
| `instructions ablate --dry-run` | conservative ablation plan for instruction candidates without editing files |
|
|
774
|
+
| `instructions apply` | safely dedupe exact duplicate instruction lines with backups |
|
|
725
775
|
| `boundaries` | multi-agent boundary check for shared files/artifacts and worktree overlap |
|
|
726
776
|
| `scan --usage` | full repo scan with local usage data |
|
|
727
777
|
| `scan --optimizer-fit` | recommend which token-optimization path fits your repo/session |
|
|
@@ -733,6 +783,7 @@ no install needed. npx runs it directly.
|
|
|
733
783
|
| `optimize` | generate `.prismo/` context packs |
|
|
734
784
|
| `context` | print paste-ready prompt for agents |
|
|
735
785
|
| `shield` | run noisy commands while keeping full output out of chat |
|
|
786
|
+
| `agent` | claim and execute safe Prismo Cloud workspace actions locally |
|
|
736
787
|
| `mcp` | expose PrismoDev tools over local MCP stdio |
|
|
737
788
|
| `setup` | detect tools, logs, proxy readiness |
|
|
738
789
|
| `usage` | show raw session token usage |
|
|
@@ -759,6 +810,14 @@ npx getprismo doctor --json # machine-readable output
|
|
|
759
810
|
|
|
760
811
|
## watch modes
|
|
761
812
|
|
|
813
|
+
```bash
|
|
814
|
+
npx getprismo guard # proactive local guard snapshot
|
|
815
|
+
npx getprismo guard --watch # keep guardrails active and sync prevention events
|
|
816
|
+
npx getprismo guard --no-sync # keep all guard events local
|
|
817
|
+
npx getprismo guard --dry-run # preview guard actions without writing state
|
|
818
|
+
npx getprismo guard --json # dashboard-ready guard payload
|
|
819
|
+
```
|
|
820
|
+
|
|
762
821
|
```bash
|
|
763
822
|
npx getprismo watch # live refresh
|
|
764
823
|
npx getprismo watch --once # single snapshot
|
|
@@ -790,6 +849,17 @@ npx getprismo shield last
|
|
|
790
849
|
npx getprismo shield search "auth failure"
|
|
791
850
|
```
|
|
792
851
|
|
|
852
|
+
### workspace agent mode
|
|
853
|
+
|
|
854
|
+
```bash
|
|
855
|
+
npx getprismo agent # claim queued workspace actions once
|
|
856
|
+
npx getprismo agent --watch # keep polling Prismo Cloud for safe actions
|
|
857
|
+
npx getprismo agent --interval 15 # poll every 15 seconds
|
|
858
|
+
npx getprismo agent --limit 3 # claim up to 3 actions per poll
|
|
859
|
+
npx getprismo agent --json # machine-readable action result
|
|
860
|
+
npx getprismo agent /path/to/repo # run actions against a specific repo
|
|
861
|
+
```
|
|
862
|
+
|
|
793
863
|
### mcp mode
|
|
794
864
|
|
|
795
865
|
```bash
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
module.exports = function createAgent(deps) {
|
|
2
|
+
const {
|
|
3
|
+
fs,
|
|
4
|
+
http,
|
|
5
|
+
https,
|
|
6
|
+
path,
|
|
7
|
+
NPX_COMMAND,
|
|
8
|
+
PACKAGE_VERSION,
|
|
9
|
+
loadConfig,
|
|
10
|
+
runDoctor,
|
|
11
|
+
runSync,
|
|
12
|
+
runGuard,
|
|
13
|
+
runShield,
|
|
14
|
+
runOptimize,
|
|
15
|
+
} = deps;
|
|
16
|
+
|
|
17
|
+
const DEFAULT_API_URL = "https://api.getprismo.dev";
|
|
18
|
+
const TERMINAL_STATUSES = new Set(["completed", "failed", "cancelled"]);
|
|
19
|
+
const SAFE_SHIELD_COMMANDS = new Set(["npm", "pnpm", "yarn", "bun", "npx", "pytest", "python", "python3", "node"]);
|
|
20
|
+
const VALID_MODES = new Set(["observe", "suggest", "autopilot"]);
|
|
21
|
+
|
|
22
|
+
function apiBase(config) {
|
|
23
|
+
return String(config?.apiUrl || DEFAULT_API_URL).replace(/\/$/, "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function requestJson(method, urlValue, token, payload, timeoutMs = 15000) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
let parsed;
|
|
29
|
+
try {
|
|
30
|
+
parsed = new URL(urlValue);
|
|
31
|
+
} catch {
|
|
32
|
+
reject(new Error(`Invalid URL: ${urlValue}`));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const body = payload ? JSON.stringify(payload) : null;
|
|
36
|
+
const client = parsed.protocol === "https:" ? https : http;
|
|
37
|
+
const request = client.request({
|
|
38
|
+
method,
|
|
39
|
+
hostname: parsed.hostname,
|
|
40
|
+
port: parsed.port,
|
|
41
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
42
|
+
timeout: timeoutMs,
|
|
43
|
+
headers: {
|
|
44
|
+
"content-type": "application/json",
|
|
45
|
+
"user-agent": `prismodev-agent/${PACKAGE_VERSION}`,
|
|
46
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
47
|
+
...(body ? { "content-length": Buffer.byteLength(body) } : {}),
|
|
48
|
+
},
|
|
49
|
+
}, (response) => {
|
|
50
|
+
const chunks = [];
|
|
51
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
52
|
+
response.on("end", () => {
|
|
53
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
54
|
+
let data = null;
|
|
55
|
+
try {
|
|
56
|
+
data = text ? JSON.parse(text) : null;
|
|
57
|
+
} catch {
|
|
58
|
+
data = { text };
|
|
59
|
+
}
|
|
60
|
+
if (response.statusCode >= 200 && response.statusCode < 300) {
|
|
61
|
+
resolve({ statusCode: response.statusCode, data });
|
|
62
|
+
} else {
|
|
63
|
+
reject(new Error(`HTTP ${response.statusCode}: ${text || response.statusMessage}`));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
request.on("timeout", () => {
|
|
68
|
+
request.destroy();
|
|
69
|
+
reject(new Error("Request timed out"));
|
|
70
|
+
});
|
|
71
|
+
request.on("error", reject);
|
|
72
|
+
if (body) request.write(body);
|
|
73
|
+
request.end();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function parseCommand(command) {
|
|
78
|
+
const parts = String(command || "").trim().split(/\s+/).filter(Boolean);
|
|
79
|
+
const getprismoIndex = parts.findIndex((part) => part === "getprismo" || part === "prismo" || part === "getprismo@latest");
|
|
80
|
+
const commandIndex = getprismoIndex >= 0 ? getprismoIndex + 1 : 0;
|
|
81
|
+
return {
|
|
82
|
+
raw: parts,
|
|
83
|
+
command: parts[commandIndex] || "",
|
|
84
|
+
args: parts.slice(commandIndex + 1),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseShieldArgs(args) {
|
|
89
|
+
const separatorIndex = args.indexOf("--");
|
|
90
|
+
const commandArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : args.slice(1);
|
|
91
|
+
if (!commandArgs.length) return null;
|
|
92
|
+
const binary = commandArgs[0];
|
|
93
|
+
if (!SAFE_SHIELD_COMMANDS.has(binary)) return null;
|
|
94
|
+
if (commandArgs.some((arg) => /[;&|`$<>]/.test(arg))) return null;
|
|
95
|
+
return commandArgs;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function repoRoot(rootDir, action) {
|
|
99
|
+
if (action.repo && path.basename(rootDir) !== action.repo) {
|
|
100
|
+
const sibling = path.join(path.dirname(rootDir), action.repo);
|
|
101
|
+
if (fs.existsSync(sibling) && fs.statSync(sibling).isDirectory()) return sibling;
|
|
102
|
+
}
|
|
103
|
+
return rootDir;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function updateAction(config, actionId, payload, options = {}) {
|
|
107
|
+
const endpoint = options.endpoint || `${apiBase(config)}/v1/dev/workspace/actions/${actionId}`;
|
|
108
|
+
const response = await requestJson("PATCH", endpoint, config.token, payload, options.timeoutMs || 15000);
|
|
109
|
+
return response.data;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function claimActions(config, options = {}) {
|
|
113
|
+
const limit = Number(options.limit || 5);
|
|
114
|
+
const endpoint = options.endpoint || `${apiBase(config)}/v1/dev/workspace/actions/claim?limit=${encodeURIComponent(limit)}`;
|
|
115
|
+
const response = await requestJson("POST", endpoint, config.token, null, options.timeoutMs || 15000);
|
|
116
|
+
return response.data?.actions || [];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function sendHeartbeat(config, payload = {}, options = {}) {
|
|
120
|
+
const endpoint = options.heartbeatEndpoint || `${apiBase(config)}/v1/dev/workspace/heartbeat`;
|
|
121
|
+
const body = {
|
|
122
|
+
agent: `prismodev/${PACKAGE_VERSION}`,
|
|
123
|
+
mode: payload.mode || "autopilot",
|
|
124
|
+
status: payload.status || "online",
|
|
125
|
+
...(payload.lastPollAt ? { lastPollAt: payload.lastPollAt } : {}),
|
|
126
|
+
};
|
|
127
|
+
const response = await requestJson("POST", endpoint, config.token, body, options.timeoutMs || 10000);
|
|
128
|
+
return response.data;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function executeAction(action, rootDir, options = {}) {
|
|
132
|
+
const root = repoRoot(path.resolve(rootDir || process.cwd()), action);
|
|
133
|
+
const parsed = parseCommand(action.command);
|
|
134
|
+
const startedAt = new Date().toISOString();
|
|
135
|
+
|
|
136
|
+
if (parsed.command === "doctor" || action.actionType === "doctor") {
|
|
137
|
+
const result = runDoctor(root, { limit: options.limit || 3, applySuggestions: true, json: true });
|
|
138
|
+
return {
|
|
139
|
+
status: "completed",
|
|
140
|
+
statusMessage: "Doctor completed and applied safe ignore/context fixes.",
|
|
141
|
+
result: {
|
|
142
|
+
command: "doctor",
|
|
143
|
+
startedAt,
|
|
144
|
+
completedAt: new Date().toISOString(),
|
|
145
|
+
score: result.after?.score ?? result.scan?.score ?? null,
|
|
146
|
+
generatedFiles: result.generatedFiles || result.optimize?.generatedFiles || [],
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (parsed.command === "sync" || action.actionType === "sync") {
|
|
152
|
+
const result = await runSync(root, { limit: options.limit || 20 });
|
|
153
|
+
return {
|
|
154
|
+
status: result.synced ? "completed" : "failed",
|
|
155
|
+
statusMessage: result.synced ? "Sync completed." : "Sync could not run because this machine is not connected.",
|
|
156
|
+
result: {
|
|
157
|
+
command: "sync",
|
|
158
|
+
startedAt,
|
|
159
|
+
completedAt: new Date().toISOString(),
|
|
160
|
+
synced: Boolean(result.synced),
|
|
161
|
+
aggregate: result.aggregate || null,
|
|
162
|
+
error: result.error || null,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (parsed.command === "guard" || action.actionType === "guard") {
|
|
168
|
+
const result = await runGuard(root, {
|
|
169
|
+
tool: "all",
|
|
170
|
+
limit: options.limit || 5,
|
|
171
|
+
tokenBudget: options.tokenBudget || 600000,
|
|
172
|
+
noSync: false,
|
|
173
|
+
watch: false,
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
status: "completed",
|
|
177
|
+
statusMessage: "Guard snapshot completed. Start agent watch mode for continuous protection.",
|
|
178
|
+
result: {
|
|
179
|
+
command: "guard",
|
|
180
|
+
startedAt,
|
|
181
|
+
completedAt: new Date().toISOString(),
|
|
182
|
+
guardRunning: Boolean(result.guardRunning),
|
|
183
|
+
events: result.events?.length || 0,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (parsed.command === "context" || parsed.command === "optimize" || action.actionType === "context") {
|
|
189
|
+
const scope = parsed.args.find((arg) => !arg.startsWith("-")) || null;
|
|
190
|
+
const result = runOptimize(root, { scope });
|
|
191
|
+
return {
|
|
192
|
+
status: "completed",
|
|
193
|
+
statusMessage: "Context pack generated.",
|
|
194
|
+
result: {
|
|
195
|
+
command: "context",
|
|
196
|
+
startedAt,
|
|
197
|
+
completedAt: new Date().toISOString(),
|
|
198
|
+
scope,
|
|
199
|
+
generatedFiles: result.generatedFiles || [],
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (parsed.command === "shield" || action.actionType === "shield") {
|
|
205
|
+
const commandArgs = parseShieldArgs(parsed.args);
|
|
206
|
+
if (!commandArgs) {
|
|
207
|
+
return {
|
|
208
|
+
status: "failed",
|
|
209
|
+
statusMessage: "Shield action was rejected because the command is not on the safe allowlist.",
|
|
210
|
+
result: { command: "shield", rejected: true, reason: "unsafe-shield-command" },
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const result = runShield(root, commandArgs);
|
|
214
|
+
return {
|
|
215
|
+
status: result.exitCode === 0 ? "completed" : "failed",
|
|
216
|
+
statusMessage: result.exitCode === 0 ? "Shielded command completed." : "Shielded command exited with an error.",
|
|
217
|
+
result: {
|
|
218
|
+
command: "shield",
|
|
219
|
+
startedAt,
|
|
220
|
+
completedAt: new Date().toISOString(),
|
|
221
|
+
exitCode: result.exitCode,
|
|
222
|
+
summary: result.summary || null,
|
|
223
|
+
runDir: result.runDir || null,
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
status: "failed",
|
|
230
|
+
statusMessage: `Unsupported workspace action: ${action.actionType || parsed.command || "unknown"}`,
|
|
231
|
+
result: {
|
|
232
|
+
rejected: true,
|
|
233
|
+
reason: "unsupported-action",
|
|
234
|
+
actionType: action.actionType,
|
|
235
|
+
command: action.command,
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function runAgentOnce(rootDir = process.cwd(), options = {}) {
|
|
241
|
+
const config = loadConfig();
|
|
242
|
+
if (!config || !config.token) {
|
|
243
|
+
return {
|
|
244
|
+
schemaVersion: 1,
|
|
245
|
+
command: "agent",
|
|
246
|
+
connected: false,
|
|
247
|
+
mode: options.mode || "autopilot",
|
|
248
|
+
actionsClaimed: 0,
|
|
249
|
+
actionsCompleted: 0,
|
|
250
|
+
actionsFailed: 0,
|
|
251
|
+
actionsObserved: 0,
|
|
252
|
+
error: "not-connected",
|
|
253
|
+
next: [`${NPX_COMMAND} connect --token <token>`],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const mode = options.mode || "autopilot";
|
|
258
|
+
const pollTime = new Date().toISOString();
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
await sendHeartbeat(config, { mode, status: "online", lastPollAt: pollTime }, options);
|
|
262
|
+
} catch (_) {}
|
|
263
|
+
|
|
264
|
+
const actions = await claimActions(config, options);
|
|
265
|
+
const results = [];
|
|
266
|
+
for (const action of actions) {
|
|
267
|
+
if (TERMINAL_STATUSES.has(action.status)) continue;
|
|
268
|
+
|
|
269
|
+
if (mode === "observe") {
|
|
270
|
+
results.push({ id: action.id, label: action.label, status: "observed", statusMessage: "Agent is in observe mode. Action not executed." });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (mode === "suggest") {
|
|
275
|
+
await updateAction(config, action.id, {
|
|
276
|
+
status: "pending_approval",
|
|
277
|
+
statusMessage: "Agent recommends this action. Waiting for approval in workspace.",
|
|
278
|
+
}, options);
|
|
279
|
+
results.push({ id: action.id, label: action.label, status: "pending_approval", statusMessage: "Suggested; awaiting approval." });
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
await updateAction(config, action.id, {
|
|
284
|
+
status: "running",
|
|
285
|
+
statusMessage: "Running locally through PrismoDev agent.",
|
|
286
|
+
}, options);
|
|
287
|
+
const result = await executeAction(action, rootDir, options);
|
|
288
|
+
await updateAction(config, action.id, result, options);
|
|
289
|
+
results.push({ id: action.id, label: action.label, ...result });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
schemaVersion: 1,
|
|
294
|
+
command: "agent",
|
|
295
|
+
connected: true,
|
|
296
|
+
mode,
|
|
297
|
+
apiUrl: apiBase(config),
|
|
298
|
+
actionsClaimed: actions.length,
|
|
299
|
+
actionsCompleted: results.filter((item) => item.status === "completed").length,
|
|
300
|
+
actionsFailed: results.filter((item) => item.status === "failed").length,
|
|
301
|
+
actionsObserved: results.filter((item) => item.status === "observed" || item.status === "pending_approval").length,
|
|
302
|
+
results,
|
|
303
|
+
privacy: {
|
|
304
|
+
rawPrompts: false,
|
|
305
|
+
rawCode: false,
|
|
306
|
+
rawStdout: false,
|
|
307
|
+
rawStderr: false,
|
|
308
|
+
fileContents: false,
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function renderAgentTerminal(result) {
|
|
314
|
+
const lines = [];
|
|
315
|
+
lines.push("");
|
|
316
|
+
lines.push("PrismoDev Agent");
|
|
317
|
+
lines.push("");
|
|
318
|
+
if (!result.connected) {
|
|
319
|
+
lines.push("Status: not connected");
|
|
320
|
+
lines.push(`Mode: ${result.mode || "autopilot"}`);
|
|
321
|
+
lines.push("");
|
|
322
|
+
lines.push("Next");
|
|
323
|
+
result.next.forEach((item, index) => lines.push(`${index + 1}. ${item}`));
|
|
324
|
+
return lines.join("\n");
|
|
325
|
+
}
|
|
326
|
+
lines.push("Status: connected");
|
|
327
|
+
lines.push(`Mode: ${result.mode}`);
|
|
328
|
+
lines.push(`API: ${result.apiUrl}`);
|
|
329
|
+
lines.push(`Actions claimed: ${result.actionsClaimed}`);
|
|
330
|
+
lines.push(`Completed: ${result.actionsCompleted}`);
|
|
331
|
+
lines.push(`Failed: ${result.actionsFailed}`);
|
|
332
|
+
if (result.actionsObserved > 0) {
|
|
333
|
+
lines.push(`Observed/Suggested: ${result.actionsObserved}`);
|
|
334
|
+
}
|
|
335
|
+
if (result.results.length) {
|
|
336
|
+
lines.push("");
|
|
337
|
+
lines.push("Actions");
|
|
338
|
+
result.results.forEach((item) => {
|
|
339
|
+
lines.push(`- ${item.status}: ${item.label}`);
|
|
340
|
+
if (item.statusMessage) lines.push(` ${item.statusMessage}`);
|
|
341
|
+
});
|
|
342
|
+
} else {
|
|
343
|
+
lines.push("");
|
|
344
|
+
lines.push("No queued workspace actions.");
|
|
345
|
+
}
|
|
346
|
+
return lines.join("\n");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function runAgent(rootDir = process.cwd(), options = {}) {
|
|
350
|
+
if (!options.watch) return runAgentOnce(rootDir, options);
|
|
351
|
+
|
|
352
|
+
const intervalMs = Math.max(5, Number(options.interval || 15)) * 1000;
|
|
353
|
+
let running = true;
|
|
354
|
+
let sleepResolve = null;
|
|
355
|
+
|
|
356
|
+
async function shutdown() {
|
|
357
|
+
if (!running) return;
|
|
358
|
+
running = false;
|
|
359
|
+
if (sleepResolve) sleepResolve();
|
|
360
|
+
try {
|
|
361
|
+
const config = loadConfig();
|
|
362
|
+
if (config?.token) {
|
|
363
|
+
await sendHeartbeat(config, { mode: options.mode || "autopilot", status: "offline" }, options);
|
|
364
|
+
}
|
|
365
|
+
} catch (_) {}
|
|
366
|
+
if (options.json) console.log(JSON.stringify({ event: "shutdown", status: "offline" }));
|
|
367
|
+
else console.log("\nAgent going offline.");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
process.on("SIGINT", shutdown);
|
|
371
|
+
process.on("SIGTERM", shutdown);
|
|
372
|
+
|
|
373
|
+
while (running) {
|
|
374
|
+
const result = await runAgentOnce(rootDir, options);
|
|
375
|
+
if (!running) break;
|
|
376
|
+
if (options.json) console.log(JSON.stringify(result, null, 2));
|
|
377
|
+
else console.log(renderAgentTerminal(result));
|
|
378
|
+
await new Promise((resolve) => {
|
|
379
|
+
sleepResolve = resolve;
|
|
380
|
+
setTimeout(resolve, intervalMs);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
process.removeListener("SIGINT", shutdown);
|
|
385
|
+
process.removeListener("SIGTERM", shutdown);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
claimActions,
|
|
390
|
+
executeAction,
|
|
391
|
+
parseCommand,
|
|
392
|
+
renderAgentTerminal,
|
|
393
|
+
runAgent,
|
|
394
|
+
runAgentOnce,
|
|
395
|
+
sendHeartbeat,
|
|
396
|
+
updateAction,
|
|
397
|
+
VALID_MODES,
|
|
398
|
+
};
|
|
399
|
+
};
|