vigthoria-cli 1.13.20 → 1.13.23
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/completions/_vigthoria +49 -0
- package/completions/vigthoria.bash +6 -0
- package/completions/vigthoria.fish +41 -0
- package/dist/commands/auth.d.ts +1 -0
- package/dist/commands/auth.js +63 -128
- package/dist/commands/background.d.ts +0 -2
- package/dist/commands/background.js +26 -34
- package/dist/commands/bridge.js +45 -41
- package/dist/commands/cancel.d.ts +1 -0
- package/dist/commands/cancel.js +22 -16
- package/dist/commands/chat.d.ts +3 -42
- package/dist/commands/chat.js +378 -1123
- package/dist/commands/coding-registration.d.ts +4 -0
- package/dist/commands/coding-registration.js +24 -0
- package/dist/commands/config.js +23 -29
- package/dist/commands/conversation-registration.d.ts +4 -0
- package/dist/commands/conversation-registration.js +122 -0
- package/dist/commands/deploy.d.ts +8 -2
- package/dist/commands/deploy.js +92 -90
- package/dist/commands/device.d.ts +1 -2
- package/dist/commands/device.js +37 -21
- package/dist/commands/doctor-registration.d.ts +4 -0
- package/dist/commands/doctor-registration.js +207 -0
- package/dist/commands/edit.js +22 -33
- package/dist/commands/explain.js +7 -10
- package/dist/commands/fork.d.ts +1 -2
- package/dist/commands/fork.js +72 -22
- package/dist/commands/game.d.ts +1 -1
- package/dist/commands/game.js +17 -6
- package/dist/commands/generate.js +7 -9
- package/dist/commands/history.js +11 -7
- package/dist/commands/hub.d.ts +2 -2
- package/dist/commands/hub.js +34 -35
- package/dist/commands/legion.d.ts +0 -1
- package/dist/commands/legion.js +62 -79
- package/dist/commands/music.d.ts +1 -2
- package/dist/commands/music.js +62 -27
- package/dist/commands/platform-registration.d.ts +4 -0
- package/dist/commands/platform-registration.js +45 -0
- package/dist/commands/preview.d.ts +0 -1
- package/dist/commands/preview.js +82 -35
- package/dist/commands/product-run-registration.d.ts +4 -0
- package/dist/commands/product-run-registration.js +490 -0
- package/dist/commands/replay.d.ts +1 -2
- package/dist/commands/replay.js +15 -11
- package/dist/commands/repo.d.ts +14 -7
- package/dist/commands/repo.js +244 -198
- package/dist/commands/review.js +9 -12
- package/dist/commands/security.d.ts +1 -2
- package/dist/commands/security.js +12 -6
- package/dist/commands/update-registration.d.ts +3 -0
- package/dist/commands/update-registration.js +368 -0
- package/dist/commands/v4-registration.d.ts +4 -0
- package/dist/commands/v4-registration.js +24 -0
- package/dist/commands/v4.d.ts +1 -1
- package/dist/commands/v4.js +46 -8
- package/dist/commands/wallet.d.ts +1 -3
- package/dist/commands/wallet.js +18 -45
- package/dist/commands/workflow.d.ts +2 -0
- package/dist/commands/workflow.js +52 -39
- package/dist/index.js +124 -1589
- package/dist/utils/agent-stream-state.d.ts +27 -0
- package/dist/utils/agent-stream-state.js +93 -0
- package/dist/utils/agentRoute.d.ts +2 -1
- package/dist/utils/agentRoute.js +22 -9
- package/dist/utils/agentRunOutcome.d.ts +16 -0
- package/dist/utils/agentRunOutcome.js +37 -2
- package/dist/utils/api-client-factory.d.ts +8 -0
- package/dist/utils/api-client-factory.js +8 -0
- package/dist/utils/api.d.ts +44 -266
- package/dist/utils/api.js +647 -3180
- package/dist/utils/auth-session.d.ts +41 -0
- package/dist/utils/auth-session.js +224 -0
- package/dist/utils/brain-hub-client.d.ts +0 -4
- package/dist/utils/brain-hub-client.js +6 -3
- package/dist/utils/bridge-client.js +2 -0
- package/dist/utils/capability-health-service.d.ts +48 -0
- package/dist/utils/capability-health-service.js +64 -0
- package/dist/utils/chat-prompt-policy.d.ts +18 -0
- package/dist/utils/chat-prompt-policy.js +61 -0
- package/dist/utils/chat-tool-call-parser.d.ts +11 -0
- package/dist/utils/chat-tool-call-parser.js +71 -0
- package/dist/utils/cli-state.d.ts +22 -4
- package/dist/utils/cli-state.js +105 -47
- package/dist/utils/code-operations-service.d.ts +104 -0
- package/dist/utils/code-operations-service.js +837 -0
- package/dist/utils/codebase-indexer.d.ts +1 -0
- package/dist/utils/codebase-indexer.js +28 -8
- package/dist/utils/command-contract.d.ts +61 -0
- package/dist/utils/command-contract.js +149 -0
- package/dist/utils/command-policy.d.ts +10 -0
- package/dist/utils/command-policy.js +54 -0
- package/dist/utils/config.d.ts +13 -5
- package/dist/utils/config.js +220 -94
- package/dist/utils/context-ranker.js +11 -2
- package/dist/utils/context-state.d.ts +39 -0
- package/dist/utils/context-state.js +52 -0
- package/dist/utils/deckEvents.d.ts +0 -5
- package/dist/utils/deckEvents.js +3 -2
- package/dist/utils/desktop-bridge-client.js +2 -0
- package/dist/utils/direct-output-policy.d.ts +3 -0
- package/dist/utils/direct-output-policy.js +76 -0
- package/dist/utils/durable-json.d.ts +57 -0
- package/dist/utils/durable-json.js +319 -0
- package/dist/utils/fastAgentRouter.d.ts +5 -6
- package/dist/utils/fastAgentRouter.js +17 -25
- package/dist/utils/files.js +21 -13
- package/dist/utils/frontend-preview-service.d.ts +76 -0
- package/dist/utils/frontend-preview-service.js +482 -0
- package/dist/utils/goaEvents.d.ts +0 -4
- package/dist/utils/goaEvents.js +5 -2
- package/dist/utils/logger.d.ts +2 -1
- package/dist/utils/logger.js +27 -20
- package/dist/utils/mcp-context-client.d.ts +24 -0
- package/dist/utils/mcp-context-client.js +58 -0
- package/dist/utils/model-governance.d.ts +18 -0
- package/dist/utils/model-governance.js +75 -0
- package/dist/utils/model-transport-service.d.ts +44 -0
- package/dist/utils/model-transport-service.js +111 -0
- package/dist/utils/mutation-journal.d.ts +42 -0
- package/dist/utils/mutation-journal.js +387 -0
- package/dist/utils/network-policy.d.ts +24 -0
- package/dist/utils/network-policy.js +202 -0
- package/dist/utils/operator-client.d.ts +45 -0
- package/dist/utils/operator-client.js +159 -0
- package/dist/utils/post-write-validator.js +7 -9
- package/dist/utils/preview-screenshot-adapter.d.ts +15 -0
- package/dist/utils/preview-screenshot-adapter.js +51 -0
- package/dist/utils/process-policy.d.ts +23 -0
- package/dist/utils/process-policy.js +284 -0
- package/dist/utils/project-memory.d.ts +13 -3
- package/dist/utils/project-memory.js +70 -17
- package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
- package/dist/utils/prompt-workspace-resolver.js +64 -0
- package/dist/utils/release-install.d.ts +11 -0
- package/dist/utils/release-install.js +155 -0
- package/dist/utils/release-policy.d.ts +64 -0
- package/dist/utils/release-policy.js +293 -0
- package/dist/utils/requestIntent.js +22 -3
- package/dist/utils/runtime-capability.d.ts +8 -0
- package/dist/utils/runtime-capability.js +16 -0
- package/dist/utils/runtime-temp.d.ts +76 -0
- package/dist/utils/runtime-temp.js +492 -0
- package/dist/utils/secret-policy.d.ts +20 -0
- package/dist/utils/secret-policy.js +160 -0
- package/dist/utils/session.d.ts +17 -3
- package/dist/utils/session.js +354 -110
- package/dist/utils/tool-approval-service.d.ts +17 -0
- package/dist/utils/tool-approval-service.js +78 -0
- package/dist/utils/tool-capability-providers.d.ts +57 -0
- package/dist/utils/tool-capability-providers.js +57 -0
- package/dist/utils/tools.d.ts +2 -23
- package/dist/utils/tools.js +226 -512
- package/dist/utils/update-policy.d.ts +18 -0
- package/dist/utils/update-policy.js +137 -0
- package/dist/utils/v3-agent-client.d.ts +40 -0
- package/dist/utils/v3-agent-client.js +165 -0
- package/dist/utils/v3-workspace-service.d.ts +28 -0
- package/dist/utils/v3-workspace-service.js +279 -0
- package/dist/utils/vigflow-client.d.ts +95 -0
- package/dist/utils/vigflow-client.js +105 -0
- package/dist/utils/workspace-boundary.d.ts +45 -0
- package/dist/utils/workspace-boundary.js +377 -0
- package/dist/utils/workspace-brain-service.d.ts +2 -0
- package/dist/utils/workspace-brain-service.js +11 -1
- package/dist/utils/workspace-cache.js +37 -15
- package/dist/utils/workspace-stream.js +35 -10
- package/install.ps1 +115 -142
- package/install.sh +112 -81
- package/package.json +43 -9
- package/release-policy.json +43 -0
- package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
- package/scripts/release/generate-completions.mjs +24 -0
- package/scripts/release/install-release.mjs +10 -0
- package/scripts/release/publish-cli-release.mjs +116 -0
- package/scripts/release/publish-cli-release.sh +2 -70
- package/scripts/release/resolve-release-manifest.mjs +26 -0
- package/scripts/release/test-balanced-model-live.sh +45 -0
- package/scripts/release/validate-live-service-gates.sh +93 -0
- package/scripts/release/validate-no-go-gates.sh +145 -174
- package/scripts/release/verify-runtime-consistency.mjs +4 -1
- package/dist/commands/index.d.ts +0 -12
- package/dist/commands/index.js +0 -194
- package/dist/commands/update.d.ts +0 -9
- package/dist/commands/update.js +0 -301
- package/dist/utils/templateInstantPath.d.ts +0 -18
- package/dist/utils/templateInstantPath.js +0 -228
package/dist/utils/session.js
CHANGED
|
@@ -5,28 +5,11 @@
|
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import * as os from 'os';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
details,
|
|
14
|
-
isCritical: true,
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
function normalizeAuthState(raw) {
|
|
18
|
-
if (!raw || typeof raw !== 'object') {
|
|
19
|
-
return { token: null, expiresAt: null, isValid: false };
|
|
20
|
-
}
|
|
21
|
-
const source = raw.auth && typeof raw.auth === 'object' ? raw.auth : raw;
|
|
22
|
-
const rawToken = source.token ?? source.accessToken ?? source.jwt ?? null;
|
|
23
|
-
const rawExpiresAt = source.expiresAt ?? null;
|
|
24
|
-
const token = typeof rawToken === 'string' && rawToken.trim().length > 0 ? rawToken.trim() : null;
|
|
25
|
-
const expiresAt = typeof rawExpiresAt === 'number' && Number.isFinite(rawExpiresAt) ? rawExpiresAt : null;
|
|
26
|
-
const session = { token, expiresAt, isValid: false };
|
|
27
|
-
session.isValid = validateSession(session, { silent: true });
|
|
28
|
-
return session;
|
|
29
|
-
}
|
|
8
|
+
import { createHash, randomUUID } from 'crypto';
|
|
9
|
+
import { secureFileMode } from './cli-state.js';
|
|
10
|
+
import { redactSensitiveText } from './secret-policy.js';
|
|
11
|
+
import { Config } from './config.js';
|
|
12
|
+
import { DurableJsonStore } from './durable-json.js';
|
|
30
13
|
/**
|
|
31
14
|
* Validate persisted authentication state without assuming any field is present.
|
|
32
15
|
*
|
|
@@ -68,27 +51,19 @@ export function validateSession(session, options = {}) {
|
|
|
68
51
|
* exception. Callers that need a hard failure can inspect ``isValid``.
|
|
69
52
|
*/
|
|
70
53
|
export async function loadSession() {
|
|
71
|
-
const configDir = path.join(os.homedir(), '.vigthoria');
|
|
72
|
-
const candidateFiles = [
|
|
73
|
-
path.join(configDir, 'auth.json'),
|
|
74
|
-
path.join(configDir, 'session.json'),
|
|
75
|
-
path.join(configDir, 'config.json'),
|
|
76
|
-
];
|
|
77
|
-
const sessionFile = candidateFiles.find((filePath) => {
|
|
78
|
-
try {
|
|
79
|
-
return fs.existsSync(filePath);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
if (!sessionFile) {
|
|
86
|
-
return { token: null, expiresAt: null, isValid: false };
|
|
87
|
-
}
|
|
88
54
|
try {
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
55
|
+
const token = new Config().get('authToken');
|
|
56
|
+
if (!token)
|
|
57
|
+
return { token: null, expiresAt: null, isValid: false };
|
|
58
|
+
const segments = token.split('.');
|
|
59
|
+
if (segments.length !== 3) {
|
|
60
|
+
// Opaque credentials were server-accepted before persistence. Their
|
|
61
|
+
// expiry cannot be inferred locally and is revalidated at the gateway.
|
|
62
|
+
return { token, expiresAt: null, isValid: true };
|
|
63
|
+
}
|
|
64
|
+
const payload = JSON.parse(Buffer.from(segments[1], 'base64url').toString('utf8'));
|
|
65
|
+
const expiresAt = typeof payload.exp === 'number' && Number.isFinite(payload.exp) ? payload.exp * 1000 : null;
|
|
66
|
+
return { token, expiresAt, isValid: expiresAt !== null && expiresAt > Date.now() };
|
|
92
67
|
}
|
|
93
68
|
catch (error) {
|
|
94
69
|
if (process.env.VIGTHORIA_DEBUG === '1') {
|
|
@@ -97,8 +72,25 @@ export async function loadSession() {
|
|
|
97
72
|
return { token: null, expiresAt: null, isValid: false };
|
|
98
73
|
}
|
|
99
74
|
}
|
|
75
|
+
const AGENT_STATUS_TRANSITIONS = {
|
|
76
|
+
submitted: new Set(['running', 'completed', 'failed', 'cancelled']),
|
|
77
|
+
retry: new Set(['running', 'completed', 'failed', 'cancelled']),
|
|
78
|
+
continue: new Set(['running', 'completed', 'failed', 'cancelled']),
|
|
79
|
+
running: new Set(['completed', 'failed', 'cancelled']),
|
|
80
|
+
completed: new Set(['retry', 'continue']),
|
|
81
|
+
failed: new Set(['retry', 'continue', 'cancelled']),
|
|
82
|
+
cancelled: new Set(['retry', 'continue']),
|
|
83
|
+
};
|
|
84
|
+
export function assertAgentStatusTransition(from, to) {
|
|
85
|
+
if (from === to)
|
|
86
|
+
return;
|
|
87
|
+
if (!AGENT_STATUS_TRANSITIONS[from]?.has(to)) {
|
|
88
|
+
throw new Error(`Invalid agent execution transition: ${from} -> ${to}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
100
91
|
export class SessionManager {
|
|
101
92
|
sessionsDir;
|
|
93
|
+
indexStore;
|
|
102
94
|
compactThreshold = 40;
|
|
103
95
|
retainRecentMessages = 18;
|
|
104
96
|
/** Keep at most this many session files on disk per CLI install. */
|
|
@@ -107,34 +99,136 @@ export class SessionManager {
|
|
|
107
99
|
retentionWindowMs = 90 * 24 * 60 * 60 * 1000;
|
|
108
100
|
/** Avoid running prune on every save — at most once per CLI process. */
|
|
109
101
|
prunedThisProcess = false;
|
|
110
|
-
constructor() {
|
|
111
|
-
this.sessionsDir = path.join(os.homedir(), '.vigthoria', 'sessions');
|
|
102
|
+
constructor(stateRoot) {
|
|
103
|
+
this.sessionsDir = path.join(stateRoot || os.homedir(), '.vigthoria', 'sessions');
|
|
112
104
|
this.ensureDir();
|
|
105
|
+
this.indexStore = new DurableJsonStore({
|
|
106
|
+
filePath: path.join(this.sessionsDir, 'index.json'),
|
|
107
|
+
schemaVersion: 1,
|
|
108
|
+
defaultValue: () => ({ sessions: {} }),
|
|
109
|
+
validate: (value) => Boolean(value && typeof value === 'object'
|
|
110
|
+
&& value.sessions
|
|
111
|
+
&& typeof value.sessions === 'object'),
|
|
112
|
+
migrate: (value) => value && typeof value === 'object' && value.sessions
|
|
113
|
+
? value
|
|
114
|
+
: { sessions: {} },
|
|
115
|
+
});
|
|
113
116
|
}
|
|
114
|
-
|
|
117
|
+
workspaceSessionsDir(project) {
|
|
118
|
+
const candidate = String(project || '').trim();
|
|
119
|
+
if (!candidate || !path.isAbsolute(candidate))
|
|
120
|
+
return null;
|
|
115
121
|
try {
|
|
116
|
-
if (!fs.existsSync(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isDirectory())
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const realWorkspace = fs.realpathSync(candidate);
|
|
130
|
+
const sessionsCandidate = path.join(realWorkspace, '.vigthoria', 'sessions');
|
|
131
|
+
const vigthoriaCandidate = path.dirname(sessionsCandidate);
|
|
132
|
+
if (fs.existsSync(vigthoriaCandidate)) {
|
|
133
|
+
const realVigthoria = fs.realpathSync(vigthoriaCandidate);
|
|
134
|
+
if (realVigthoria !== path.join(realWorkspace, '.vigthoria'))
|
|
135
|
+
return null;
|
|
124
136
|
}
|
|
137
|
+
return sessionsCandidate;
|
|
125
138
|
}
|
|
126
|
-
catch
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
sessionStore(filePath, fallback) {
|
|
144
|
+
return new DurableJsonStore({
|
|
145
|
+
filePath,
|
|
146
|
+
schemaVersion: 3,
|
|
147
|
+
defaultValue: () => fallback,
|
|
148
|
+
validate: (value) => Boolean(value && typeof value === 'object'
|
|
149
|
+
&& typeof value.id === 'string'
|
|
150
|
+
&& typeof value.project === 'string'
|
|
151
|
+
&& Array.isArray(value.messages)),
|
|
152
|
+
migrate: (value) => {
|
|
153
|
+
if (!value || typeof value !== 'object')
|
|
154
|
+
throw new Error('Session is not an object.');
|
|
155
|
+
const legacy = value;
|
|
156
|
+
if (!legacy.id || !legacy.project || !Array.isArray(legacy.messages))
|
|
157
|
+
throw new Error('Session identity is incomplete.');
|
|
158
|
+
if (legacy.activeAgentRun) {
|
|
159
|
+
const allowed = new Set(['submitted', 'retry', 'continue', 'running', 'completed', 'failed', 'cancelled']);
|
|
160
|
+
if (!allowed.has(legacy.activeAgentRun.status))
|
|
161
|
+
legacy.activeAgentRun.status = 'failed';
|
|
136
162
|
}
|
|
163
|
+
return this.ensureMessageIds(legacy);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
ensureMessageIds(session) {
|
|
168
|
+
const hadIds = Array.isArray(session.messageIds);
|
|
169
|
+
const ids = hadIds ? [...session.messageIds] : [];
|
|
170
|
+
if (ids.length > session.messages.length)
|
|
171
|
+
ids.length = session.messages.length;
|
|
172
|
+
for (let index = ids.length; index < session.messages.length; index += 1) {
|
|
173
|
+
ids.push(hadIds
|
|
174
|
+
? randomUUID()
|
|
175
|
+
: `legacy-${createHash('sha256').update(`${session.id}\0${index}\0${JSON.stringify(session.messages[index])}`).digest('hex')}`);
|
|
176
|
+
}
|
|
177
|
+
const seen = new Set();
|
|
178
|
+
session.messageIds = ids.map((id) => {
|
|
179
|
+
const normalized = typeof id === 'string' && id.trim() ? id.trim() : randomUUID();
|
|
180
|
+
if (seen.has(normalized)) {
|
|
181
|
+
const replacement = randomUUID();
|
|
182
|
+
seen.add(replacement);
|
|
183
|
+
return replacement;
|
|
137
184
|
}
|
|
185
|
+
seen.add(normalized);
|
|
186
|
+
return normalized;
|
|
187
|
+
});
|
|
188
|
+
return session;
|
|
189
|
+
}
|
|
190
|
+
readSessionFile(filePath) {
|
|
191
|
+
try {
|
|
192
|
+
let parsed;
|
|
193
|
+
try {
|
|
194
|
+
parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
parsed = JSON.parse(fs.readFileSync(`${filePath}.bak`, 'utf8'));
|
|
198
|
+
}
|
|
199
|
+
const fallbackCandidate = parsed && typeof parsed === 'object' && 'data' in parsed
|
|
200
|
+
? parsed.data
|
|
201
|
+
: parsed;
|
|
202
|
+
const fallback = fallbackCandidate;
|
|
203
|
+
if (!fallback?.id || !fallback?.project || !Array.isArray(fallback.messages))
|
|
204
|
+
return null;
|
|
205
|
+
return this.ensureMessageIds(this.sessionStore(filePath, fallback).read());
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
readSessionsFromDir(directory) {
|
|
212
|
+
if (!directory)
|
|
213
|
+
return [];
|
|
214
|
+
try {
|
|
215
|
+
return fs.readdirSync(directory)
|
|
216
|
+
.filter((name) => name.endsWith('.json'))
|
|
217
|
+
.map((name) => {
|
|
218
|
+
return this.readSessionFile(path.join(directory, name));
|
|
219
|
+
})
|
|
220
|
+
.filter((session) => Boolean(session?.id && session?.project));
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
ensureDir() {
|
|
227
|
+
if (!fs.existsSync(this.sessionsDir)) {
|
|
228
|
+
fs.mkdirSync(this.sessionsDir, { recursive: true, mode: 0o700 });
|
|
229
|
+
}
|
|
230
|
+
if (process.platform !== 'win32') {
|
|
231
|
+
fs.chmodSync(this.sessionsDir, 0o700);
|
|
138
232
|
}
|
|
139
233
|
}
|
|
140
234
|
/**
|
|
@@ -149,7 +243,7 @@ export class SessionManager {
|
|
|
149
243
|
try {
|
|
150
244
|
const now = Date.now();
|
|
151
245
|
const files = fs.readdirSync(this.sessionsDir)
|
|
152
|
-
.filter((f) => f.endsWith('.json'))
|
|
246
|
+
.filter((f) => f.endsWith('.json') && f !== 'index.json')
|
|
153
247
|
.map((f) => {
|
|
154
248
|
const full = path.join(this.sessionsDir, f);
|
|
155
249
|
let mtimeMs = 0;
|
|
@@ -163,7 +257,7 @@ export class SessionManager {
|
|
|
163
257
|
for (const entry of files) {
|
|
164
258
|
if (entry.mtimeMs === 0 || now - entry.mtimeMs > this.retentionWindowMs) {
|
|
165
259
|
try {
|
|
166
|
-
|
|
260
|
+
this.delete(path.basename(entry.file, '.json'));
|
|
167
261
|
}
|
|
168
262
|
catch { /* ignore */ }
|
|
169
263
|
}
|
|
@@ -175,7 +269,7 @@ export class SessionManager {
|
|
|
175
269
|
if (remaining.length > this.maxRetainedSessions) {
|
|
176
270
|
for (const entry of remaining.slice(this.maxRetainedSessions)) {
|
|
177
271
|
try {
|
|
178
|
-
|
|
272
|
+
this.delete(path.basename(entry.file, '.json'));
|
|
179
273
|
}
|
|
180
274
|
catch { /* ignore */ }
|
|
181
275
|
}
|
|
@@ -193,6 +287,34 @@ export class SessionManager {
|
|
|
193
287
|
const random = Math.random().toString(36).substring(2, 8);
|
|
194
288
|
return `${timestamp}-${random}`;
|
|
195
289
|
}
|
|
290
|
+
getProjectId(project, create) {
|
|
291
|
+
const workspaceDir = this.workspaceSessionsDir(project);
|
|
292
|
+
if (!workspaceDir)
|
|
293
|
+
return undefined;
|
|
294
|
+
const vigthoriaDir = path.dirname(workspaceDir);
|
|
295
|
+
const marker = path.join(vigthoriaDir, 'project-id');
|
|
296
|
+
try {
|
|
297
|
+
const existing = fs.readFileSync(marker, 'utf8').trim();
|
|
298
|
+
if (/^[a-f0-9-]{20,64}$/i.test(existing))
|
|
299
|
+
return existing.toLowerCase();
|
|
300
|
+
}
|
|
301
|
+
catch { /* marker not created yet */ }
|
|
302
|
+
if (!create)
|
|
303
|
+
return undefined;
|
|
304
|
+
fs.mkdirSync(vigthoriaDir, { recursive: true, mode: 0o700 });
|
|
305
|
+
const projectId = randomUUID();
|
|
306
|
+
try {
|
|
307
|
+
fs.writeFileSync(marker, `${projectId}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
308
|
+
secureFileMode(marker);
|
|
309
|
+
return projectId;
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
if (error.code === 'EEXIST') {
|
|
313
|
+
return fs.readFileSync(marker, 'utf8').trim().toLowerCase();
|
|
314
|
+
}
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
196
318
|
/**
|
|
197
319
|
* Create a new session
|
|
198
320
|
*/
|
|
@@ -203,12 +325,14 @@ export class SessionManager {
|
|
|
203
325
|
project,
|
|
204
326
|
model,
|
|
205
327
|
messages: [],
|
|
328
|
+
messageIds: [],
|
|
206
329
|
memorySummary: '',
|
|
207
330
|
compactedAt: null,
|
|
208
331
|
createdAt: new Date().toISOString(),
|
|
209
332
|
updatedAt: new Date().toISOString(),
|
|
210
333
|
agentMode,
|
|
211
334
|
operatorMode,
|
|
335
|
+
projectId: this.getProjectId(project, true),
|
|
212
336
|
};
|
|
213
337
|
this.save(session);
|
|
214
338
|
return session;
|
|
@@ -219,37 +343,89 @@ export class SessionManager {
|
|
|
219
343
|
*/
|
|
220
344
|
save(session) {
|
|
221
345
|
const compacted = this.compactSession(session);
|
|
222
|
-
const targetSession = compacted.session;
|
|
346
|
+
const targetSession = this.ensureMessageIds(compacted.session);
|
|
223
347
|
targetSession.updatedAt = new Date().toISOString();
|
|
348
|
+
targetSession.projectId = targetSession.projectId || this.getProjectId(targetSession.project, true);
|
|
224
349
|
const filePath = path.join(this.sessionsDir, `${session.id}.json`);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
350
|
+
const persisted = this.sessionStore(filePath, targetSession).update((current) => this.mergeConcurrentSession(current, targetSession)).data;
|
|
351
|
+
secureFileMode(filePath);
|
|
352
|
+
const mirrors = [];
|
|
353
|
+
// Mirror agent checkpoints into the bound workspace. This makes interrupted
|
|
354
|
+
// direct runs resumable when a packaged/sandboxed CLI cannot write the
|
|
355
|
+
// process home directory, and keeps state attached to the correct project
|
|
356
|
+
// across terminals and external client machines.
|
|
357
|
+
if (persisted.activeAgentRun || persisted.lastAgentRunOutcome) {
|
|
358
|
+
const workspaceDir = this.workspaceSessionsDir(persisted.project);
|
|
359
|
+
if (workspaceDir) {
|
|
360
|
+
fs.mkdirSync(workspaceDir, { recursive: true, mode: 0o700 });
|
|
361
|
+
const workspaceFile = path.join(workspaceDir, `${persisted.id}.json`);
|
|
362
|
+
this.sessionStore(workspaceFile, persisted).update((current) => this.mergeConcurrentSession(current, persisted));
|
|
363
|
+
secureFileMode(workspaceFile);
|
|
364
|
+
mirrors.push(workspaceFile);
|
|
233
365
|
}
|
|
234
366
|
}
|
|
367
|
+
this.indexStore.update((index) => ({
|
|
368
|
+
sessions: {
|
|
369
|
+
...index.sessions,
|
|
370
|
+
[persisted.id]: {
|
|
371
|
+
projectId: persisted.projectId,
|
|
372
|
+
project: persisted.project,
|
|
373
|
+
mirrors: [...new Set([...(index.sessions[persisted.id]?.mirrors || []), ...mirrors])],
|
|
374
|
+
updatedAt: persisted.updatedAt,
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
}));
|
|
235
378
|
this.pruneIfNeeded();
|
|
236
379
|
}
|
|
380
|
+
mergeConcurrentSession(current, incoming) {
|
|
381
|
+
if (current.id !== incoming.id)
|
|
382
|
+
return incoming;
|
|
383
|
+
this.ensureMessageIds(current);
|
|
384
|
+
this.ensureMessageIds(incoming);
|
|
385
|
+
const compactedChanged = Boolean(incoming.compactedAt && incoming.compactedAt !== current.compactedAt);
|
|
386
|
+
const messages = compactedChanged ? incoming.messages : [...current.messages];
|
|
387
|
+
const messageIds = compactedChanged ? [...incoming.messageIds] : [...current.messageIds];
|
|
388
|
+
if (!compactedChanged) {
|
|
389
|
+
const indexById = new Map(messageIds.map((id, index) => [id, index]));
|
|
390
|
+
for (let index = 0; index < incoming.messages.length; index += 1) {
|
|
391
|
+
const message = incoming.messages[index];
|
|
392
|
+
const id = incoming.messageIds[index];
|
|
393
|
+
const existingIndex = indexById.get(id);
|
|
394
|
+
if (existingIndex === undefined) {
|
|
395
|
+
indexById.set(id, messages.length);
|
|
396
|
+
messageIds.push(id);
|
|
397
|
+
messages.push(message);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
messages[existingIndex] = message;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const checkpointTimes = [incoming.activeAgentRun, current.activeAgentRun]
|
|
405
|
+
.filter((checkpoint) => Boolean(checkpoint))
|
|
406
|
+
.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
|
|
407
|
+
return {
|
|
408
|
+
...current,
|
|
409
|
+
...incoming,
|
|
410
|
+
messages,
|
|
411
|
+
messageIds,
|
|
412
|
+
activeAgentRun: checkpointTimes[0] || incoming.activeAgentRun || current.activeAgentRun,
|
|
413
|
+
lastAgentRunOutcome: incoming.lastAgentRunOutcome || current.lastAgentRunOutcome,
|
|
414
|
+
updatedAt: new Date(Math.max(Date.parse(current.updatedAt) || 0, Date.parse(incoming.updatedAt) || 0)).toISOString(),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
237
417
|
/**
|
|
238
418
|
* Load session by ID
|
|
239
419
|
*/
|
|
240
420
|
load(id) {
|
|
421
|
+
const indexRecord = this.indexStore.read().sessions[id];
|
|
422
|
+
if (indexRecord?.deletedAt)
|
|
423
|
+
return null;
|
|
241
424
|
const filePath = path.join(this.sessionsDir, `${id}.json`);
|
|
242
425
|
if (!fs.existsSync(filePath)) {
|
|
243
426
|
return null;
|
|
244
427
|
}
|
|
245
|
-
|
|
246
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
247
|
-
return JSON.parse(content);
|
|
248
|
-
}
|
|
249
|
-
catch (error) {
|
|
250
|
-
console.warn(`Failed to load session ${id}:`, error);
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
428
|
+
return this.readSessionFile(filePath);
|
|
253
429
|
}
|
|
254
430
|
normalizeProjectKey(projectPath) {
|
|
255
431
|
const raw = String(projectPath || '').trim();
|
|
@@ -273,24 +449,30 @@ export class SessionManager {
|
|
|
273
449
|
* Get the most recent session for a project
|
|
274
450
|
*/
|
|
275
451
|
getLatest(project) {
|
|
276
|
-
const
|
|
452
|
+
const primarySessions = this.list()
|
|
453
|
+
.map((metadata) => this.load(metadata.id))
|
|
454
|
+
.filter((session) => Boolean(session));
|
|
455
|
+
const workspaceSessions = this.readSessionsFromDir(this.workspaceSessionsDir(project));
|
|
456
|
+
const sessionsById = new Map();
|
|
457
|
+
const index = this.indexStore.read();
|
|
458
|
+
for (const session of [...primarySessions, ...workspaceSessions]) {
|
|
459
|
+
if (index.sessions[session.id]?.deletedAt)
|
|
460
|
+
continue;
|
|
461
|
+
const existing = sessionsById.get(session.id);
|
|
462
|
+
if (!existing || new Date(session.updatedAt).getTime() >= new Date(existing.updatedAt).getTime()) {
|
|
463
|
+
sessionsById.set(session.id, session);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const sessions = [...sessionsById.values()];
|
|
277
467
|
const projectKey = this.normalizeProjectKey(project);
|
|
278
|
-
const
|
|
468
|
+
const projectId = this.getProjectId(project, false);
|
|
279
469
|
const exactMatches = sessions
|
|
280
|
-
.filter((s) =>
|
|
470
|
+
.filter((s) => projectId && s.projectId
|
|
471
|
+
? s.projectId === projectId
|
|
472
|
+
: this.normalizeProjectKey(s.project) === projectKey)
|
|
281
473
|
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
282
474
|
if (exactMatches.length > 0) {
|
|
283
|
-
return
|
|
284
|
-
}
|
|
285
|
-
// Fallback: if old sessions stored inconsistent slash/case forms, allow
|
|
286
|
-
// latest same-folder match (e.g., C:\Proj and C:/proj) to preserve /retry UX.
|
|
287
|
-
if (projectBase) {
|
|
288
|
-
const softMatches = sessions
|
|
289
|
-
.filter((s) => path.basename(this.normalizeProjectKey(s.project) || '').toLowerCase() === projectBase)
|
|
290
|
-
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
|
291
|
-
if (softMatches.length > 0) {
|
|
292
|
-
return this.load(softMatches[0].id);
|
|
293
|
-
}
|
|
475
|
+
return exactMatches[0];
|
|
294
476
|
}
|
|
295
477
|
return null;
|
|
296
478
|
}
|
|
@@ -301,16 +483,18 @@ export class SessionManager {
|
|
|
301
483
|
list() {
|
|
302
484
|
let files = [];
|
|
303
485
|
try {
|
|
304
|
-
files = fs.readdirSync(this.sessionsDir).filter(f => f.endsWith('.json'));
|
|
486
|
+
files = fs.readdirSync(this.sessionsDir).filter(f => f.endsWith('.json') && f !== 'index.json');
|
|
305
487
|
}
|
|
306
488
|
catch {
|
|
307
489
|
return [];
|
|
308
490
|
}
|
|
309
491
|
return files.map(f => {
|
|
310
492
|
try {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
493
|
+
const session = this.readSessionFile(path.join(this.sessionsDir, f));
|
|
494
|
+
if (!session || this.indexStore.read().sessions[session.id]?.deletedAt)
|
|
495
|
+
return null;
|
|
496
|
+
const { messages, messageIds: _messageIds, ...metadata } = session;
|
|
497
|
+
void _messageIds;
|
|
314
498
|
return { ...metadata, messages: [] };
|
|
315
499
|
}
|
|
316
500
|
catch (error) {
|
|
@@ -326,20 +510,73 @@ export class SessionManager {
|
|
|
326
510
|
*/
|
|
327
511
|
delete(id) {
|
|
328
512
|
const filePath = path.join(this.sessionsDir, `${id}.json`);
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
513
|
+
const index = this.indexStore.read();
|
|
514
|
+
const record = index.sessions[id];
|
|
515
|
+
const targets = [filePath, ...(record?.mirrors || [])];
|
|
516
|
+
let removed = false;
|
|
517
|
+
for (const target of targets) {
|
|
518
|
+
if (path.basename(target) !== `${id}.json` || path.basename(path.dirname(target)) !== 'sessions')
|
|
519
|
+
continue;
|
|
520
|
+
try {
|
|
521
|
+
fs.unlinkSync(target);
|
|
522
|
+
removed = true;
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
if (error.code !== 'ENOENT')
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
332
528
|
}
|
|
333
|
-
|
|
529
|
+
this.indexStore.update((current) => ({
|
|
530
|
+
sessions: {
|
|
531
|
+
...current.sessions,
|
|
532
|
+
[id]: {
|
|
533
|
+
projectId: record?.projectId,
|
|
534
|
+
project: record?.project || '',
|
|
535
|
+
mirrors: record?.mirrors || [],
|
|
536
|
+
updatedAt: record?.updatedAt || new Date().toISOString(),
|
|
537
|
+
deletedAt: new Date().toISOString(),
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
}));
|
|
541
|
+
return removed;
|
|
334
542
|
}
|
|
335
543
|
/**
|
|
336
544
|
* Clear all sessions
|
|
337
545
|
*/
|
|
338
546
|
clearAll() {
|
|
339
547
|
const files = fs.readdirSync(this.sessionsDir)
|
|
340
|
-
.filter(f => f.endsWith('.json'));
|
|
341
|
-
|
|
342
|
-
|
|
548
|
+
.filter(f => f.endsWith('.json') && f !== 'index.json');
|
|
549
|
+
const ids = new Set([
|
|
550
|
+
...files.map((file) => path.basename(file, '.json')),
|
|
551
|
+
...Object.keys(this.indexStore.read().sessions),
|
|
552
|
+
]);
|
|
553
|
+
let removed = 0;
|
|
554
|
+
for (const id of ids) {
|
|
555
|
+
if (this.delete(id))
|
|
556
|
+
removed += 1;
|
|
557
|
+
}
|
|
558
|
+
return removed;
|
|
559
|
+
}
|
|
560
|
+
markCancelledByContext(contextId) {
|
|
561
|
+
let updated = 0;
|
|
562
|
+
for (const metadata of this.list()) {
|
|
563
|
+
const session = this.load(metadata.id);
|
|
564
|
+
const checkpoint = session?.activeAgentRun;
|
|
565
|
+
if (!session || !checkpoint || checkpoint.contextId !== contextId)
|
|
566
|
+
continue;
|
|
567
|
+
if (!['submitted', 'retry', 'continue', 'running', 'failed'].includes(checkpoint.status))
|
|
568
|
+
continue;
|
|
569
|
+
assertAgentStatusTransition(checkpoint.status, 'cancelled');
|
|
570
|
+
session.activeAgentRun = {
|
|
571
|
+
...checkpoint,
|
|
572
|
+
status: 'cancelled',
|
|
573
|
+
updatedAt: new Date().toISOString(),
|
|
574
|
+
error: 'Cancelled by user request.',
|
|
575
|
+
};
|
|
576
|
+
this.save(session);
|
|
577
|
+
updated += 1;
|
|
578
|
+
}
|
|
579
|
+
return updated;
|
|
343
580
|
}
|
|
344
581
|
/**
|
|
345
582
|
* Add message to session
|
|
@@ -358,13 +595,16 @@ export class SessionManager {
|
|
|
358
595
|
return [
|
|
359
596
|
'Session memory summary from earlier conversation turns.',
|
|
360
597
|
'Use this as compressed context for follow-up questions and resume behavior.',
|
|
361
|
-
session.memorySummary.trim(),
|
|
598
|
+
redactSensitiveText(session.memorySummary.trim()),
|
|
362
599
|
].join('\n');
|
|
363
600
|
}
|
|
364
601
|
compactSession(session) {
|
|
602
|
+
this.ensureMessageIds(session);
|
|
365
603
|
if (session.messages.length <= this.compactThreshold) {
|
|
366
604
|
return { session, compacted: false };
|
|
367
605
|
}
|
|
606
|
+
const priorMessages = [...session.messages];
|
|
607
|
+
const priorIds = [...session.messageIds];
|
|
368
608
|
const systemMessages = session.messages.filter((message) => message.role === 'system' && this.isPersistentSystemMessage(message.content));
|
|
369
609
|
const recentMessages = session.messages.slice(-this.retainRecentMessages);
|
|
370
610
|
const olderMessages = session.messages.slice(0, -this.retainRecentMessages);
|
|
@@ -372,6 +612,10 @@ export class SessionManager {
|
|
|
372
612
|
session.memorySummary = nextSummary;
|
|
373
613
|
session.compactedAt = new Date().toISOString();
|
|
374
614
|
session.messages = this.mergePersistentAndRecent(systemMessages, recentMessages);
|
|
615
|
+
session.messageIds = session.messages.map((message) => {
|
|
616
|
+
const index = priorMessages.indexOf(message);
|
|
617
|
+
return index >= 0 ? priorIds[index] : randomUUID();
|
|
618
|
+
});
|
|
375
619
|
return { session, compacted: true };
|
|
376
620
|
}
|
|
377
621
|
isPersistentSystemMessage(content) {
|
|
@@ -434,7 +678,7 @@ export class SessionManager {
|
|
|
434
678
|
return summaryLines.slice(-14).join('\n');
|
|
435
679
|
}
|
|
436
680
|
normalizeMessageContent(content) {
|
|
437
|
-
const collapsed = String(content || '')
|
|
681
|
+
const collapsed = redactSensitiveText(String(content || ''))
|
|
438
682
|
.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, ' ')
|
|
439
683
|
.replace(/\s+/g, ' ')
|
|
440
684
|
.trim();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Logger } from './logger.js';
|
|
2
|
+
import type { ApprovalScope } from './process-policy.js';
|
|
3
|
+
export declare class ToolApprovalService {
|
|
4
|
+
private readonly workspace;
|
|
5
|
+
private readonly logger;
|
|
6
|
+
private readonly sessionOperations;
|
|
7
|
+
private readonly store;
|
|
8
|
+
constructor(workspace: string, logger: Logger, permissionsFile?: string);
|
|
9
|
+
private report;
|
|
10
|
+
private read;
|
|
11
|
+
hasPersistent(scope: ApprovalScope): boolean;
|
|
12
|
+
hasSession(scope: ApprovalScope): boolean;
|
|
13
|
+
recordSession(scope: ApprovalScope): void;
|
|
14
|
+
recordPersistent(scope: ApprovalScope): void;
|
|
15
|
+
clearSession(): void;
|
|
16
|
+
sessionKeys(): string[];
|
|
17
|
+
}
|