vigthoria-cli 1.13.20 → 1.13.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/completions/_vigthoria +49 -0
  2. package/completions/vigthoria.bash +6 -0
  3. package/completions/vigthoria.fish +41 -0
  4. package/dist/commands/auth.d.ts +1 -0
  5. package/dist/commands/auth.js +63 -128
  6. package/dist/commands/background.d.ts +0 -2
  7. package/dist/commands/background.js +26 -34
  8. package/dist/commands/bridge.js +45 -41
  9. package/dist/commands/cancel.d.ts +1 -0
  10. package/dist/commands/cancel.js +22 -16
  11. package/dist/commands/chat.d.ts +3 -42
  12. package/dist/commands/chat.js +378 -1123
  13. package/dist/commands/coding-registration.d.ts +4 -0
  14. package/dist/commands/coding-registration.js +24 -0
  15. package/dist/commands/config.js +23 -29
  16. package/dist/commands/conversation-registration.d.ts +4 -0
  17. package/dist/commands/conversation-registration.js +122 -0
  18. package/dist/commands/deploy.d.ts +8 -2
  19. package/dist/commands/deploy.js +92 -90
  20. package/dist/commands/device.d.ts +1 -2
  21. package/dist/commands/device.js +37 -21
  22. package/dist/commands/doctor-registration.d.ts +4 -0
  23. package/dist/commands/doctor-registration.js +200 -0
  24. package/dist/commands/edit.js +22 -33
  25. package/dist/commands/explain.js +7 -10
  26. package/dist/commands/fork.d.ts +1 -2
  27. package/dist/commands/fork.js +72 -22
  28. package/dist/commands/game.d.ts +1 -1
  29. package/dist/commands/game.js +17 -6
  30. package/dist/commands/generate.js +7 -9
  31. package/dist/commands/history.js +11 -7
  32. package/dist/commands/hub.d.ts +2 -2
  33. package/dist/commands/hub.js +34 -35
  34. package/dist/commands/legion.d.ts +0 -1
  35. package/dist/commands/legion.js +62 -79
  36. package/dist/commands/music.d.ts +1 -2
  37. package/dist/commands/music.js +62 -27
  38. package/dist/commands/platform-registration.d.ts +4 -0
  39. package/dist/commands/platform-registration.js +45 -0
  40. package/dist/commands/preview.d.ts +0 -1
  41. package/dist/commands/preview.js +82 -35
  42. package/dist/commands/product-run-registration.d.ts +4 -0
  43. package/dist/commands/product-run-registration.js +490 -0
  44. package/dist/commands/replay.d.ts +1 -2
  45. package/dist/commands/replay.js +15 -11
  46. package/dist/commands/repo.d.ts +14 -7
  47. package/dist/commands/repo.js +245 -196
  48. package/dist/commands/review.js +9 -12
  49. package/dist/commands/security.d.ts +1 -2
  50. package/dist/commands/security.js +12 -6
  51. package/dist/commands/update-registration.d.ts +3 -0
  52. package/dist/commands/update-registration.js +338 -0
  53. package/dist/commands/v4-registration.d.ts +4 -0
  54. package/dist/commands/v4-registration.js +24 -0
  55. package/dist/commands/v4.d.ts +1 -1
  56. package/dist/commands/v4.js +46 -8
  57. package/dist/commands/wallet.d.ts +1 -3
  58. package/dist/commands/wallet.js +18 -45
  59. package/dist/commands/workflow.d.ts +2 -0
  60. package/dist/commands/workflow.js +52 -39
  61. package/dist/index.js +122 -1589
  62. package/dist/utils/agent-stream-state.d.ts +27 -0
  63. package/dist/utils/agent-stream-state.js +93 -0
  64. package/dist/utils/agentRoute.d.ts +2 -1
  65. package/dist/utils/agentRoute.js +22 -9
  66. package/dist/utils/agentRunOutcome.d.ts +16 -0
  67. package/dist/utils/agentRunOutcome.js +37 -2
  68. package/dist/utils/api-client-factory.d.ts +8 -0
  69. package/dist/utils/api-client-factory.js +8 -0
  70. package/dist/utils/api.d.ts +44 -266
  71. package/dist/utils/api.js +647 -3180
  72. package/dist/utils/auth-session.d.ts +41 -0
  73. package/dist/utils/auth-session.js +224 -0
  74. package/dist/utils/brain-hub-client.d.ts +0 -4
  75. package/dist/utils/brain-hub-client.js +6 -3
  76. package/dist/utils/bridge-client.js +2 -0
  77. package/dist/utils/capability-health-service.d.ts +48 -0
  78. package/dist/utils/capability-health-service.js +64 -0
  79. package/dist/utils/chat-prompt-policy.d.ts +18 -0
  80. package/dist/utils/chat-prompt-policy.js +61 -0
  81. package/dist/utils/chat-tool-call-parser.d.ts +11 -0
  82. package/dist/utils/chat-tool-call-parser.js +71 -0
  83. package/dist/utils/cli-state.d.ts +22 -4
  84. package/dist/utils/cli-state.js +105 -47
  85. package/dist/utils/code-operations-service.d.ts +104 -0
  86. package/dist/utils/code-operations-service.js +837 -0
  87. package/dist/utils/codebase-indexer.d.ts +1 -0
  88. package/dist/utils/codebase-indexer.js +28 -8
  89. package/dist/utils/command-contract.d.ts +61 -0
  90. package/dist/utils/command-contract.js +149 -0
  91. package/dist/utils/command-policy.d.ts +10 -0
  92. package/dist/utils/command-policy.js +54 -0
  93. package/dist/utils/config.d.ts +13 -5
  94. package/dist/utils/config.js +220 -94
  95. package/dist/utils/context-ranker.js +11 -2
  96. package/dist/utils/context-state.d.ts +39 -0
  97. package/dist/utils/context-state.js +52 -0
  98. package/dist/utils/deckEvents.d.ts +0 -5
  99. package/dist/utils/deckEvents.js +3 -2
  100. package/dist/utils/desktop-bridge-client.js +2 -0
  101. package/dist/utils/direct-output-policy.d.ts +3 -0
  102. package/dist/utils/direct-output-policy.js +76 -0
  103. package/dist/utils/durable-json.d.ts +57 -0
  104. package/dist/utils/durable-json.js +319 -0
  105. package/dist/utils/fastAgentRouter.d.ts +5 -6
  106. package/dist/utils/fastAgentRouter.js +17 -25
  107. package/dist/utils/files.js +21 -13
  108. package/dist/utils/frontend-preview-service.d.ts +76 -0
  109. package/dist/utils/frontend-preview-service.js +482 -0
  110. package/dist/utils/goaEvents.d.ts +0 -4
  111. package/dist/utils/goaEvents.js +5 -2
  112. package/dist/utils/logger.d.ts +2 -1
  113. package/dist/utils/logger.js +27 -20
  114. package/dist/utils/mcp-context-client.d.ts +24 -0
  115. package/dist/utils/mcp-context-client.js +58 -0
  116. package/dist/utils/model-governance.d.ts +18 -0
  117. package/dist/utils/model-governance.js +75 -0
  118. package/dist/utils/model-transport-service.d.ts +44 -0
  119. package/dist/utils/model-transport-service.js +111 -0
  120. package/dist/utils/mutation-journal.d.ts +42 -0
  121. package/dist/utils/mutation-journal.js +387 -0
  122. package/dist/utils/network-policy.d.ts +24 -0
  123. package/dist/utils/network-policy.js +202 -0
  124. package/dist/utils/operator-client.d.ts +45 -0
  125. package/dist/utils/operator-client.js +159 -0
  126. package/dist/utils/post-write-validator.js +7 -9
  127. package/dist/utils/preview-screenshot-adapter.d.ts +13 -0
  128. package/dist/utils/preview-screenshot-adapter.js +36 -0
  129. package/dist/utils/process-policy.d.ts +23 -0
  130. package/dist/utils/process-policy.js +284 -0
  131. package/dist/utils/project-memory.d.ts +13 -3
  132. package/dist/utils/project-memory.js +70 -17
  133. package/dist/utils/prompt-workspace-resolver.d.ts +14 -0
  134. package/dist/utils/prompt-workspace-resolver.js +64 -0
  135. package/dist/utils/release-install.d.ts +11 -0
  136. package/dist/utils/release-install.js +155 -0
  137. package/dist/utils/release-policy.d.ts +64 -0
  138. package/dist/utils/release-policy.js +293 -0
  139. package/dist/utils/requestIntent.js +22 -3
  140. package/dist/utils/runtime-capability.d.ts +8 -0
  141. package/dist/utils/runtime-capability.js +16 -0
  142. package/dist/utils/secret-policy.d.ts +20 -0
  143. package/dist/utils/secret-policy.js +160 -0
  144. package/dist/utils/session.d.ts +17 -3
  145. package/dist/utils/session.js +354 -110
  146. package/dist/utils/tool-approval-service.d.ts +17 -0
  147. package/dist/utils/tool-approval-service.js +78 -0
  148. package/dist/utils/tool-capability-providers.d.ts +57 -0
  149. package/dist/utils/tool-capability-providers.js +57 -0
  150. package/dist/utils/tools.d.ts +2 -23
  151. package/dist/utils/tools.js +216 -508
  152. package/dist/utils/update-policy.d.ts +18 -0
  153. package/dist/utils/update-policy.js +137 -0
  154. package/dist/utils/v3-agent-client.d.ts +40 -0
  155. package/dist/utils/v3-agent-client.js +165 -0
  156. package/dist/utils/v3-workspace-service.d.ts +28 -0
  157. package/dist/utils/v3-workspace-service.js +279 -0
  158. package/dist/utils/vigflow-client.d.ts +95 -0
  159. package/dist/utils/vigflow-client.js +105 -0
  160. package/dist/utils/workspace-boundary.d.ts +45 -0
  161. package/dist/utils/workspace-boundary.js +377 -0
  162. package/dist/utils/workspace-brain-service.d.ts +2 -0
  163. package/dist/utils/workspace-brain-service.js +11 -1
  164. package/dist/utils/workspace-cache.js +37 -15
  165. package/dist/utils/workspace-stream.js +35 -10
  166. package/install.ps1 +85 -142
  167. package/install.sh +75 -80
  168. package/package.json +43 -9
  169. package/release-policy.json +43 -0
  170. package/scripts/release/LOCAL_MACHINE_USER_VERIFICATION.md +1 -1
  171. package/scripts/release/generate-completions.mjs +24 -0
  172. package/scripts/release/install-release.mjs +10 -0
  173. package/scripts/release/publish-cli-release.mjs +116 -0
  174. package/scripts/release/publish-cli-release.sh +2 -70
  175. package/scripts/release/resolve-release-manifest.mjs +26 -0
  176. package/scripts/release/test-balanced-model-live.sh +42 -0
  177. package/scripts/release/validate-live-service-gates.sh +90 -0
  178. package/scripts/release/validate-no-go-gates.sh +135 -174
  179. package/scripts/release/verify-runtime-consistency.mjs +4 -1
  180. package/dist/commands/index.d.ts +0 -12
  181. package/dist/commands/index.js +0 -194
  182. package/dist/commands/update.d.ts +0 -9
  183. package/dist/commands/update.js +0 -301
  184. package/dist/utils/templateInstantPath.d.ts +0 -18
  185. package/dist/utils/templateInstantPath.js +0 -228
@@ -2,7 +2,12 @@
2
2
  * Configuration Manager for Vigthoria CLI
3
3
  */
4
4
  import Conf from 'conf';
5
+ import * as fs from 'fs';
6
+ import * as os from 'os';
7
+ import * as path from 'path';
5
8
  import { secureFileMode } from './cli-state.js';
9
+ import { DurableJsonStore } from './durable-json.js';
10
+ import { assertTrustedEndpoint, assertTrustedWebSocketEndpoint, guardedFetch } from './network-policy.js';
6
11
  const defaultConfig = {
7
12
  apiUrl: 'https://coder.vigthoria.io',
8
13
  modelsApiUrl: 'https://api.vigthoria.io', // Direct AI Models API - no proxying through Coder
@@ -43,95 +48,213 @@ const defaultConfig = {
43
48
  ],
44
49
  },
45
50
  };
51
+ function cloneDefaultConfig() {
52
+ return JSON.parse(JSON.stringify(defaultConfig));
53
+ }
54
+ function readJsonObject(filePath) {
55
+ try {
56
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
57
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ function optionalString(value) {
64
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
65
+ }
66
+ function normalizeConfigValue(value, base = cloneDefaultConfig()) {
67
+ const raw = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
68
+ const next = JSON.parse(JSON.stringify(base));
69
+ const legacyUser = raw.user && typeof raw.user === 'object' ? raw.user : {};
70
+ for (const key of ['apiUrl', 'modelsApiUrl', 'musicApiUrl', 'wsUrl']) {
71
+ if (typeof raw[key] === 'string' && raw[key])
72
+ next[key] = String(raw[key]);
73
+ }
74
+ if (raw.selfHostedModelsApiUrl === null || typeof raw.selfHostedModelsApiUrl === 'string')
75
+ next.selfHostedModelsApiUrl = raw.selfHostedModelsApiUrl;
76
+ next.authToken = optionalString(raw.authToken ?? raw.token) ?? next.authToken;
77
+ next.refreshToken = optionalString(raw.refreshToken) ?? next.refreshToken;
78
+ next.userId = optionalString(raw.userId ?? legacyUser.id) ?? next.userId;
79
+ next.email = optionalString(raw.email ?? legacyUser.email) ?? next.email;
80
+ next.v3ServiceKey = optionalString(raw.v3ServiceKey) ?? next.v3ServiceKey;
81
+ if (raw.subscription && typeof raw.subscription === 'object' && !Array.isArray(raw.subscription)) {
82
+ const subscription = raw.subscription;
83
+ next.subscription = {
84
+ plan: optionalString(subscription.plan),
85
+ status: optionalString(subscription.status),
86
+ expiresAt: optionalString(subscription.expiresAt ?? subscription.expires_at),
87
+ };
88
+ }
89
+ if (raw.preferences && typeof raw.preferences === 'object' && !Array.isArray(raw.preferences)) {
90
+ next.preferences = { ...next.preferences, ...raw.preferences };
91
+ }
92
+ if (raw.project && typeof raw.project === 'object' && !Array.isArray(raw.project)) {
93
+ const project = raw.project;
94
+ next.project = {
95
+ rootPath: project.rootPath === null ? null : optionalString(project.rootPath) ?? next.project.rootPath,
96
+ ignorePatterns: Array.isArray(project.ignorePatterns)
97
+ ? project.ignorePatterns.filter((entry) => typeof entry === 'string')
98
+ : next.project.ignorePatterns,
99
+ };
100
+ }
101
+ if (raw.persona === 'default' || raw.persona === 'wiener_grant')
102
+ next.persona = raw.persona;
103
+ if (raw.hubModelPrefs && typeof raw.hubModelPrefs === 'object' && !Array.isArray(raw.hubModelPrefs)) {
104
+ const prefs = raw.hubModelPrefs;
105
+ next.hubModelPrefs = {
106
+ enabledCloudModels: Array.isArray(prefs.enabledCloudModels)
107
+ ? prefs.enabledCloudModels.filter((entry) => typeof entry === 'string')
108
+ : [],
109
+ defaultCloudModel: optionalString(prefs.defaultCloudModel) || undefined,
110
+ defaultLocalModel: optionalString(prefs.defaultLocalModel) || undefined,
111
+ balance: typeof prefs.balance === 'number' && Number.isFinite(prefs.balance) ? prefs.balance : undefined,
112
+ fetchedAt: optionalString(prefs.fetchedAt) || undefined,
113
+ };
114
+ }
115
+ return next;
116
+ }
117
+ function isConfigValue(value) {
118
+ if (!value || typeof value !== 'object' || Array.isArray(value))
119
+ return false;
120
+ const candidate = value;
121
+ return typeof candidate.apiUrl === 'string'
122
+ && typeof candidate.modelsApiUrl === 'string'
123
+ && typeof candidate.musicApiUrl === 'string'
124
+ && typeof candidate.wsUrl === 'string'
125
+ && Boolean(candidate.subscription && typeof candidate.subscription === 'object')
126
+ && Boolean(candidate.preferences && typeof candidate.preferences === 'object')
127
+ && Boolean(candidate.project && typeof candidate.project === 'object')
128
+ && Array.isArray(candidate.project.ignorePatterns);
129
+ }
46
130
  export class Config {
47
131
  store;
48
132
  static OPERATOR_PLANS = new Set(['enterprise', 'admin', 'master_admin']);
49
- static CLOUD_PLANS = new Set(['pro', 'professional', 'ultra', 'enterprise', 'master_admin', 'admin']);
50
- constructor() {
51
- // `Conf` defaults to 0o644 on POSIX which would leave auth tokens
52
- // readable by every local user. We post-process the file in
53
- // `secureOnDisk()` to enforce 0o600 ownership. The `fileMode`
54
- // option exists on newer `conf` releases — supply it via a cast so
55
- // older type-defs do not reject the field.
56
- this.store = new Conf({
57
- projectName: 'vigthoria-cli',
58
- defaults: defaultConfig,
59
- ...{ fileMode: 0o600 },
60
- schema: {
61
- apiUrl: { type: 'string' },
62
- modelsApiUrl: { type: 'string' },
63
- musicApiUrl: { type: 'string' },
64
- wsUrl: { type: 'string' },
65
- selfHostedModelsApiUrl: { type: ['string', 'null'] },
66
- authToken: { type: ['string', 'null'] },
67
- refreshToken: { type: ['string', 'null'] },
68
- userId: { type: ['string', 'null'] },
69
- email: { type: ['string', 'null'] },
70
- v3ServiceKey: { type: ['string', 'null'] },
71
- subscription: {
72
- type: 'object',
73
- properties: {
74
- plan: { type: ['string', 'null'] },
75
- status: { type: ['string', 'null'] },
76
- expiresAt: { type: ['string', 'null'] },
77
- },
78
- },
79
- preferences: {
80
- type: 'object',
81
- properties: {
82
- defaultModel: { type: 'string' },
83
- theme: { type: 'string', enum: ['dark', 'light'] },
84
- autoApplyFixes: { type: 'boolean' },
85
- showDiffs: { type: 'boolean' },
86
- contextLines: { type: 'number' },
87
- maxTokens: { type: 'number' },
88
- },
89
- },
90
- persona: { type: 'string', enum: ['default', 'wiener_grant'] },
91
- project: {
92
- type: 'object',
93
- properties: {
94
- rootPath: { type: ['string', 'null'] },
95
- ignorePatterns: { type: 'array', items: { type: 'string' } },
96
- },
97
- },
133
+ constructor(options = {}) {
134
+ const stateRoot = path.resolve(options.stateRoot || os.homedir());
135
+ const canonicalPath = path.join(stateRoot, '.vigthoria', 'config.json');
136
+ const legacyConfPath = options.legacyConfPath
137
+ ? path.resolve(options.legacyConfPath)
138
+ : options.stateRoot
139
+ ? path.join(stateRoot, '.legacy-conf', 'config.json')
140
+ : new Conf({ projectName: 'vigthoria-cli' }).path;
141
+ const legacyCandidate = path.resolve(legacyConfPath) === path.resolve(canonicalPath) ? null : readJsonObject(legacyConfPath);
142
+ const legacyEnvelopeData = legacyCandidate?.data && typeof legacyCandidate.data === 'object'
143
+ ? legacyCandidate.data
144
+ : null;
145
+ const legacyConf = legacyEnvelopeData?.credentialsRetired === true ? null : legacyCandidate;
146
+ const legacyMtime = legacyConf ? fs.statSync(legacyConfPath).mtimeMs : 0;
147
+ const canonicalMtime = fs.existsSync(canonicalPath) ? fs.statSync(canonicalPath).mtimeMs : 0;
148
+ this.store = new DurableJsonStore({
149
+ filePath: canonicalPath,
150
+ schemaVersion: 2,
151
+ defaultValue: cloneDefaultConfig,
152
+ validate: isConfigValue,
153
+ migrate: (value) => {
154
+ const fromConf = legacyConf ? normalizeConfigValue(legacyConf) : cloneDefaultConfig();
155
+ const merged = normalizeConfigValue(value, fromConf);
156
+ // During the one-time collision resolution, preserve the most
157
+ // recently written credential bundle. Non-auth preferences use
158
+ // canonical-file precedence.
159
+ if (legacyConf && legacyMtime > canonicalMtime) {
160
+ const legacyCredentials = normalizeConfigValue(legacyConf);
161
+ merged.authToken = legacyCredentials.authToken;
162
+ merged.refreshToken = legacyCredentials.refreshToken;
163
+ merged.userId = legacyCredentials.userId;
164
+ merged.email = legacyCredentials.email;
165
+ merged.v3ServiceKey = legacyCredentials.v3ServiceKey;
166
+ merged.subscription = legacyCredentials.subscription;
167
+ }
168
+ return merged;
98
169
  },
99
170
  });
100
- // Defence-in-depth: re-apply 0600 every time we instantiate Config so
101
- // that an older install which created the file with 0644 gets locked
102
- // down on next startup.
103
- try {
104
- secureFileMode(this.store.path);
171
+ const canonicalExisted = fs.existsSync(canonicalPath);
172
+ if (!canonicalExisted && legacyConf) {
173
+ this.store.write(normalizeConfigValue(legacyConf));
174
+ }
175
+ else {
176
+ // Triggers legacy-flat migration and corruption recovery before any
177
+ // command can observe configuration.
178
+ this.store.read();
105
179
  }
106
- catch { /* best-effort */ }
180
+ if (legacyConf)
181
+ this.retireLegacyConfStore(legacyConfPath, canonicalPath);
182
+ this.secureOnDisk();
183
+ }
184
+ retireLegacyConfStore(legacyPath, canonicalPath) {
185
+ const retired = new DurableJsonStore({
186
+ filePath: legacyPath,
187
+ schemaVersion: 1,
188
+ defaultValue: () => ({ migratedTo: canonicalPath, credentialsRetired: true }),
189
+ validate: (value) => Boolean(value && typeof value === 'object'
190
+ && typeof value.migratedTo === 'string'
191
+ && value.credentialsRetired === true),
192
+ migrate: () => ({ migratedTo: canonicalPath, credentialsRetired: true }),
193
+ });
194
+ retired.write({ migratedTo: canonicalPath, credentialsRetired: true });
107
195
  }
108
196
  /**
109
197
  * Re-apply secure POSIX permissions to the on-disk config file. Safe
110
198
  * to call repeatedly; no-op on Windows.
111
199
  */
112
200
  secureOnDisk() {
113
- try {
114
- secureFileMode(this.store.path);
115
- }
116
- catch { /* best-effort */ }
201
+ secureFileMode(this.store.filePath);
202
+ }
203
+ validateEndpointValue(key, value) {
204
+ if (typeof value !== 'string')
205
+ return value;
206
+ if (key === 'apiUrl')
207
+ assertTrustedEndpoint(value, { audience: 'coder', ignoreOffline: true });
208
+ else if (key === 'modelsApiUrl' || key === 'selfHostedModelsApiUrl')
209
+ assertTrustedEndpoint(value, { audience: 'models', ignoreOffline: true });
210
+ else if (key === 'musicApiUrl')
211
+ assertTrustedEndpoint(value, { audience: 'music', ignoreOffline: true });
212
+ else if (key === 'wsUrl')
213
+ assertTrustedWebSocketEndpoint(value, 'coder', false, true);
214
+ return value;
117
215
  }
118
216
  get(key) {
119
- return this.store.get(key);
217
+ return this.validateEndpointValue(key, this.store.read()[key]);
120
218
  }
121
219
  set(key, value) {
122
- this.store.set(key, value);
220
+ const validated = this.validateEndpointValue(key, value);
221
+ this.store.update((current) => ({ ...current, [key]: validated }));
222
+ this.secureOnDisk();
123
223
  }
124
224
  getAll() {
125
- return this.store.store;
225
+ const current = this.store.read();
226
+ return {
227
+ ...current,
228
+ apiUrl: this.validateEndpointValue('apiUrl', current.apiUrl),
229
+ modelsApiUrl: this.validateEndpointValue('modelsApiUrl', current.modelsApiUrl),
230
+ musicApiUrl: this.validateEndpointValue('musicApiUrl', current.musicApiUrl),
231
+ wsUrl: this.validateEndpointValue('wsUrl', current.wsUrl),
232
+ selfHostedModelsApiUrl: current.selfHostedModelsApiUrl
233
+ ? this.validateEndpointValue('selfHostedModelsApiUrl', current.selfHostedModelsApiUrl)
234
+ : null,
235
+ };
126
236
  }
127
237
  reset() {
128
- this.store.clear();
238
+ this.store.write(cloneDefaultConfig());
239
+ }
240
+ resetPreservingAuth() {
241
+ this.store.update((current) => {
242
+ const next = cloneDefaultConfig();
243
+ next.authToken = current.authToken;
244
+ next.refreshToken = current.refreshToken;
245
+ next.userId = current.userId;
246
+ next.email = current.email;
247
+ next.v3ServiceKey = current.v3ServiceKey;
248
+ next.subscription = { ...current.subscription };
249
+ return next;
250
+ });
251
+ this.secureOnDisk();
129
252
  }
130
253
  isAuthenticated() {
131
- return this.store.get('authToken') !== null;
254
+ return Boolean(this.get('authToken'));
132
255
  }
133
256
  hasValidSubscription() {
134
- const sub = this.store.get('subscription');
257
+ const sub = this.get('subscription');
135
258
  if (!sub.status || sub.status !== 'active')
136
259
  return false;
137
260
  if (sub.expiresAt) {
@@ -141,7 +264,7 @@ export class Config {
141
264
  return true;
142
265
  }
143
266
  getNormalizedPlan() {
144
- return (this.store.get('subscription').plan || '').trim().toLowerCase();
267
+ return (this.get('subscription').plan || '').trim().toLowerCase();
145
268
  }
146
269
  hasOperatorAccess() {
147
270
  return Config.OPERATOR_PLANS.has(this.getNormalizedPlan());
@@ -153,42 +276,45 @@ export class Config {
153
276
  return this.isAuthenticated();
154
277
  }
155
278
  getConfigPath() {
156
- return this.store.path;
279
+ return this.store.filePath;
157
280
  }
158
281
  // Auth helpers
159
282
  setAuth(data) {
160
- this.store.set('authToken', data.token);
161
- if (data.refreshToken) {
162
- this.store.set('refreshToken', data.refreshToken);
163
- }
164
- this.store.set('userId', data.userId);
165
- this.store.set('email', data.email);
283
+ this.store.update((current) => ({
284
+ ...current,
285
+ authToken: data.token,
286
+ refreshToken: data.refreshToken === undefined ? current.refreshToken : data.refreshToken,
287
+ userId: data.userId === undefined ? current.userId : data.userId,
288
+ email: data.email === undefined ? current.email : data.email,
289
+ apiUrl: data.apiUrl ? this.validateEndpointValue('apiUrl', data.apiUrl) : current.apiUrl,
290
+ v3ServiceKey: data.v3ServiceKey === undefined ? current.v3ServiceKey : data.v3ServiceKey,
291
+ }));
166
292
  // Tighten permissions whenever the token rotates so a brand-new
167
293
  // install (which may have created the file under a previous default
168
294
  // umask) becomes 0600 immediately after first login.
169
295
  this.secureOnDisk();
170
296
  }
171
297
  clearAuth() {
172
- this.store.set('authToken', null);
173
- this.store.set('refreshToken', null);
174
- this.store.set('userId', null);
175
- this.store.set('email', null);
176
- this.store.set('subscription', {
177
- plan: null,
178
- status: null,
179
- expiresAt: null,
180
- });
298
+ this.store.update((current) => ({
299
+ ...current,
300
+ authToken: null,
301
+ refreshToken: null,
302
+ userId: null,
303
+ email: null,
304
+ v3ServiceKey: null,
305
+ hubModelPrefs: undefined,
306
+ subscription: { plan: null, status: null, expiresAt: null },
307
+ }));
181
308
  }
182
309
  setSubscription(data) {
183
- this.store.set('subscription', {
184
- plan: data.plan,
185
- status: data.status,
186
- expiresAt: data.expiresAt || null,
187
- });
310
+ this.store.update((current) => ({
311
+ ...current,
312
+ subscription: { plan: data.plan, status: data.status, expiresAt: data.expiresAt || null },
313
+ }));
188
314
  }
189
315
  // Model selection - Vigthoria Models (internal routing IDs only)
190
316
  getAvailableModels() {
191
- const sub = this.store.get('subscription');
317
+ const sub = this.get('subscription');
192
318
  const plan = (sub.plan || '').toLowerCase();
193
319
  void plan;
194
320
  // ═══════════════════════════════════════════════════════════════
@@ -208,28 +334,28 @@ export class Config {
208
334
  // Legacy aliases
209
335
  { id: 'cloud', name: 'Vigthoria Cloud Balanced', description: 'Legacy alias for cloud-balanced', tier: 'cloud', backendModel: 'vigthoria-cloud-balanced' }, { id: 'cloud-reason', name: 'Vigthoria Cloud Balanced', description: 'Legacy alias for cloud-balanced', tier: 'cloud', backendModel: 'vigthoria-cloud-balanced' }, { id: 'ultra', name: 'Vigthoria Cloud Maximum', description: 'Legacy alias for cloud-maximum', tier: 'cloud', backendModel: 'vigthoria-cloud-maximum' });
210
336
  }
211
- const hubPrefs = this.store.get('hubModelPrefs');
337
+ const hubPrefs = this.get('hubModelPrefs');
212
338
  if (hubPrefs?.enabledCloudModels?.length) {
213
339
  return this.filterModelsByHubPreferences(models, hubPrefs.enabledCloudModels);
214
340
  }
215
341
  return models;
216
342
  }
217
343
  async refreshHubModelPreferences() {
218
- const token = this.store.get('authToken');
219
- const apiUrl = String(this.store.get('apiUrl') || '').replace(/\/$/, '');
344
+ const token = this.get('authToken');
345
+ const apiUrl = String(this.get('apiUrl') || '').replace(/\/$/, '');
220
346
  if (!token || !apiUrl)
221
347
  return;
222
348
  try {
223
- const response = await fetch(`${apiUrl}/api/user/credits/model-preferences`, {
349
+ const response = await guardedFetch(`${apiUrl}/api/user/credits/model-preferences`, {
224
350
  headers: { Authorization: `Bearer ${token}` },
225
351
  signal: AbortSignal.timeout(8000),
226
- });
352
+ }, { audience: 'coder' });
227
353
  if (!response.ok)
228
354
  return;
229
355
  const prefs = await response.json();
230
356
  if (prefs?.success === false)
231
357
  return;
232
- this.store.set('hubModelPrefs', {
358
+ this.set('hubModelPrefs', {
233
359
  enabledCloudModels: Array.isArray(prefs.enabledCloudModels) ? prefs.enabledCloudModels : [],
234
360
  defaultCloudModel: typeof prefs.defaultCloudModel === 'string' ? prefs.defaultCloudModel : undefined,
235
361
  defaultLocalModel: typeof prefs.defaultLocalModel === 'string' ? prefs.defaultLocalModel : undefined,
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import fs from 'node:fs';
11
11
  import path from 'node:path';
12
+ import { containsHighConfidenceSecret, isSensitivePath } from './secret-policy.js';
12
13
  const IGNORED_DIRS = new Set([
13
14
  '.git', 'node_modules', 'dist', '.next', '__pycache__',
14
15
  '.vigthoria', 'coverage', '.cache', '.turbo', 'build', 'out',
@@ -16,7 +17,7 @@ const IGNORED_DIRS = new Set([
16
17
  const TEXT_EXTENSIONS = new Set([
17
18
  '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
18
19
  '.py', '.go', '.rs', '.java', '.c', '.cpp', '.h', '.cs', '.php', '.rb',
19
- '.json', '.yaml', '.yml', '.toml', '.env', '.sh', '.bash',
20
+ '.json', '.yaml', '.yml', '.toml', '.sh', '.bash',
20
21
  '.html', '.css', '.scss', '.sass', '.less',
21
22
  '.md', '.txt', '.xml', '.svg',
22
23
  ]);
@@ -86,6 +87,9 @@ function walkWorkspace(dir, maxFiles = 800) {
86
87
  if (IGNORED_DIRS.has(entry.name))
87
88
  continue;
88
89
  const fullPath = path.join(current, entry.name);
90
+ const relativePath = path.relative(workspaceRoot, fullPath).replace(/\\/g, '/');
91
+ if (isSensitivePath(relativePath))
92
+ continue;
89
93
  // Security: skip symlinks that escape workspace root (fixes 8.2)
90
94
  if (entry.isSymbolicLink()) {
91
95
  try {
@@ -101,7 +105,12 @@ function walkWorkspace(dir, maxFiles = 800) {
101
105
  stack.push(fullPath);
102
106
  }
103
107
  else if (entry.isFile() && TEXT_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
104
- results.push(fullPath);
108
+ try {
109
+ const content = fs.readFileSync(fullPath, 'utf8').slice(0, 300000);
110
+ if (!containsHighConfidenceSecret(content))
111
+ results.push(fullPath);
112
+ }
113
+ catch { /* unreadable files are not context */ }
105
114
  }
106
115
  }
107
116
  }
@@ -0,0 +1,39 @@
1
+ export type ContextStateSource = 'workflow-memory' | 'session-memory' | 'project-memory' | 'codebase-index' | 'mcp-context' | 'account-brain' | 'workspace-hashes';
2
+ export interface ContextOwnershipRule {
3
+ owner: string;
4
+ schema: string;
5
+ precedence: number;
6
+ privacy: 'local-only' | 'account-remote' | 'execution-remote' | 'metadata-only';
7
+ retention: string;
8
+ conflictPolicy: string;
9
+ }
10
+ export declare const CONTEXT_STATE_OWNERSHIP: Readonly<Record<ContextStateSource, ContextOwnershipRule>>;
11
+ export interface ContextStateLayer {
12
+ source: ContextStateSource;
13
+ schema: string;
14
+ precedence: number;
15
+ privacy: ContextOwnershipRule['privacy'];
16
+ content?: string;
17
+ metadata?: Record<string, unknown>;
18
+ contentHash?: string;
19
+ }
20
+ export interface ContextStateEnvelope {
21
+ schema: 'vigthoria.context-state.v1';
22
+ precedence: ContextStateSource[];
23
+ layers: ContextStateLayer[];
24
+ compatibilityAdapters: {
25
+ projectMemory: 'project-memory';
26
+ codebaseContext: 'codebase-index';
27
+ accountBrainContext: 'account-brain';
28
+ vigthoriaBrain: 'project-memory-metadata';
29
+ };
30
+ }
31
+ export declare function assembleContextState(input: Partial<Record<ContextStateSource, {
32
+ content?: string;
33
+ metadata?: Record<string, unknown>;
34
+ }>>): ContextStateEnvelope;
35
+ export declare function contextLayer(envelope: ContextStateEnvelope, source: ContextStateSource): ContextStateLayer | undefined;
36
+ export declare function appendContextLayer(envelope: ContextStateEnvelope | undefined, source: ContextStateSource, value: {
37
+ content?: string;
38
+ metadata?: Record<string, unknown>;
39
+ }): ContextStateEnvelope;
@@ -0,0 +1,52 @@
1
+ import { createHash } from 'crypto';
2
+ import { scanOutboundContext } from './secret-policy.js';
3
+ export const CONTEXT_STATE_OWNERSHIP = Object.freeze({
4
+ 'workflow-memory': { owner: 'VigFlow target adapter', schema: 'vigthoria.context.workflow.v1', precedence: 90, privacy: 'execution-remote', retention: 'remote workflow policy', conflictPolicy: 'explicit workflow inputs override remembered context' },
5
+ 'session-memory': { owner: 'SessionManager', schema: 'vigthoria.context.session.v1', precedence: 80, privacy: 'local-only', retention: '90 days / 200 sessions', conflictPolicy: 'latest project-bound checkpoint wins' },
6
+ 'project-memory': { owner: 'ProjectMemoryService', schema: 'vigthoria.context.project-memory.v2', precedence: 70, privacy: 'local-only', retention: '240 bounded items', conflictPolicy: 'newer explicit decisions/preferences win by item identity' },
7
+ 'codebase-index': { owner: 'CodebaseIndexer', schema: 'vigthoria.context.codebase-index.v1', precedence: 60, privacy: 'local-only', retention: 'content-addressed until reindex', conflictPolicy: 'current workspace bytes are authoritative' },
8
+ 'mcp-context': { owner: 'MCP execution binding', schema: 'vigthoria.context.mcp.v1', precedence: 50, privacy: 'execution-remote', retention: 'remote execution lifetime', conflictPolicy: 'scoped tool evidence is additive and cannot override local files' },
9
+ 'account-brain': { owner: 'BrainHubClient', schema: 'vigthoria.context.account-brain.v1', precedence: 40, privacy: 'account-remote', retention: 'account Brain policy', conflictPolicy: 'account facts yield to project/session evidence' },
10
+ 'workspace-hashes': { owner: 'WorkspaceHashCache', schema: 'vigthoria.context.workspace-hashes.v1', precedence: 10, privacy: 'metadata-only', retention: 'latest successful workspace observation', conflictPolicy: 'routing metadata only; never semantic authority' },
11
+ });
12
+ export function assembleContextState(input) {
13
+ const layers = Object.entries(input)
14
+ .filter((entry) => Boolean(entry[1]))
15
+ .map(([source, value]) => {
16
+ const rule = CONTEXT_STATE_OWNERSHIP[source];
17
+ const scanned = scanOutboundContext(value).value;
18
+ const content = String(scanned.content || '').trim();
19
+ const contentHash = content ? createHash('sha256').update(content).digest('hex') : undefined;
20
+ return {
21
+ source,
22
+ schema: rule.schema,
23
+ precedence: rule.precedence,
24
+ privacy: rule.privacy,
25
+ ...(content ? { content, contentHash } : {}),
26
+ ...(scanned.metadata ? { metadata: scanned.metadata } : {}),
27
+ };
28
+ })
29
+ .sort((left, right) => right.precedence - left.precedence)
30
+ .filter((layer, _index, all) => !layer.contentHash
31
+ || all.findIndex((candidate) => candidate.contentHash === layer.contentHash) === all.indexOf(layer));
32
+ return {
33
+ schema: 'vigthoria.context-state.v1',
34
+ precedence: layers.map((layer) => layer.source),
35
+ layers,
36
+ compatibilityAdapters: {
37
+ projectMemory: 'project-memory',
38
+ codebaseContext: 'codebase-index',
39
+ accountBrainContext: 'account-brain',
40
+ vigthoriaBrain: 'project-memory-metadata',
41
+ },
42
+ };
43
+ }
44
+ export function contextLayer(envelope, source) {
45
+ return envelope.layers.find((layer) => layer.source === source);
46
+ }
47
+ export function appendContextLayer(envelope, source, value) {
48
+ const existing = envelope?.layers || [];
49
+ const input = Object.fromEntries(existing.map((layer) => [layer.source, { content: layer.content, metadata: layer.metadata }]));
50
+ input[source] = value;
51
+ return assembleContextState(input);
52
+ }
@@ -1,8 +1,3 @@
1
- /**
2
- * Structured events for Vigthoria Workbench (VIGTHORIA_DECK_MODE=1).
3
- * Emitted as single lines: @vigthoria-deck:{json}
4
- * Workbench strips these from terminal display and updates UI panels.
5
- */
6
1
  export type DeckEventType = 'agent_start' | 'tool_start' | 'tool_end' | 'report_ready' | 'run_complete' | 'run_summary';
7
2
  export interface DeckEvent {
8
3
  type: DeckEventType;
@@ -3,6 +3,7 @@
3
3
  * Emitted as single lines: @vigthoria-deck:{json}
4
4
  * Workbench strips these from terminal display and updates UI panels.
5
5
  */
6
+ import { scanOutboundContext } from './secret-policy.js';
6
7
  const DECK_PREFIX = '@vigthoria-deck:';
7
8
  export function isDeckModeEnabled() {
8
9
  return /^(1|true|yes)$/i.test(String(process.env.VIGTHORIA_DECK_MODE || ''));
@@ -10,10 +11,10 @@ export function isDeckModeEnabled() {
10
11
  export function emitDeckEvent(event) {
11
12
  if (!isDeckModeEnabled())
12
13
  return;
13
- const payload = {
14
+ const payload = scanOutboundContext({
14
15
  ...event,
15
16
  ts: event.ts ?? Date.now(),
16
- };
17
+ }).value;
17
18
  // Force the marker onto its own fresh line. Callers may have just updated an
18
19
  // in-place spinner (e.g. ora) which redraws via "\r" and never emits a
19
20
  // trailing "\n", so without this leading newline the marker can land stuck
@@ -1,9 +1,11 @@
1
+ import { assertTrustedEndpoint } from './network-policy.js';
1
2
  export async function getDesktopBridgeStatus(options = {}) {
2
3
  const endpoint = options.url
3
4
  || process.env.VIGTHORIA_DESKTOP_BRIDGE_URL
4
5
  || `http://${process.env.VIGTHORIA_DESKTOP_HOST || '127.0.0.1'}:${process.env.VIGTHORIA_DESKTOP_PORT || '49160'}`;
5
6
  const timeoutMs = options.timeoutMs ?? 5000;
6
7
  try {
8
+ assertTrustedEndpoint(endpoint, { audience: 'bridge' });
7
9
  const controller = new AbortController();
8
10
  const timer = setTimeout(() => controller.abort(), timeoutMs);
9
11
  const response = await fetch(`${endpoint.replace(/\/$/, '')}/health`, {
@@ -0,0 +1,3 @@
1
+ export declare function stripHiddenThoughtBlocks(text: string): string;
2
+ export declare function sanitizeDirectModeOutput(text: string): string;
3
+ export declare function isDirectModeFollowUpQuestion(text: string): boolean;