vigthoria-cli 1.13.18 → 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 +84 -135
  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 +5 -42
  12. package/dist/commands/chat.js +462 -1122
  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 +32 -42
  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 -1498
  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 +45 -269
  71. package/dist/utils/api.js +664 -3210
  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 +227 -99
  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 +6 -7
  106. package/dist/utils/fastAgentRouter.js +21 -30
  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 +34 -2
  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 +86 -143
  167. package/install.sh +76 -81
  168. package/package.json +44 -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 -172
  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,81 +276,86 @@ 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
  // ═══════════════════════════════════════════════════════════════
195
321
  // Vigthoria server infrastructure operational models
196
322
  // ═══════════════════════════════════════════════════════════════
197
323
  const models = [
198
- { id: 'code', name: 'Vigthoria v3 Code 35B', description: 'Native 35B coding model on Blackwell (V3 pipeline)', tier: 'local', backendModel: 'vigthoria-v3-code-35b' },
199
- { id: 'code-35b', name: 'Vigthoria v3 Code 35B', description: 'Same flagship model as code', tier: 'local', backendModel: 'vigthoria-v3-code-35b' },
200
- { id: 'code-9b', name: 'Vigthoria v3 Code 9B', description: 'Efficient coding specialist for quick tasks', tier: 'local', backendModel: 'vigthoria-v3-code-9b' },
201
- { id: 'balanced', name: 'Vigthoria Balanced 4B', description: 'Balanced general-purpose local model', tier: 'local', backendModel: 'vigthoria-v3-balanced-4b' },
202
- { id: 'balanced-4b', name: 'Vigthoria Balanced 4B', description: 'Efficient 4B general-purpose model (Qwen3.5-4B based)', tier: 'local', backendModel: 'vigthoria-v3-balanced-4b' },
324
+ { id: 'architect', name: 'Vigthoria v4 Creative 27B', description: 'Architect, planning, deep reasoning, and creative systems work', tier: 'local', backendModel: 'Vigthoria-v4-Creative-27B' },
325
+ { id: 'code', name: 'Vigthoria v4 Code 27B', description: 'Dense production executor for coding, debugging, and repository changes', tier: 'local', backendModel: 'Vigthoria-v4-Code-27B' },
326
+ { id: 'assistant', name: 'Vigthoria v4 Assistant 9B', description: 'Low-latency FIM, completion, voice, and diagnostic model', tier: 'local', backendModel: 'Vigthoria-v4-Assistant-9B' },
327
+ { id: 'code-35b', name: 'Vigthoria v4 Code 27B', description: 'Legacy selector mapped to the V4 executor', tier: 'local', backendModel: 'Vigthoria-v4-Code-27B' },
328
+ { id: 'code-9b', name: 'Vigthoria v4 Assistant 9B', description: 'Legacy selector mapped to the V4 assistant', tier: 'local', backendModel: 'Vigthoria-v4-Assistant-9B' },
329
+ { id: 'balanced', name: 'Vigthoria v4 Assistant 9B', description: 'Legacy selector mapped to the V4 assistant', tier: 'local', backendModel: 'Vigthoria-v4-Assistant-9B' },
330
+ { id: 'balanced-4b', name: 'Vigthoria v4 Assistant 9B', description: 'Legacy selector mapped to the V4 assistant', tier: 'local', backendModel: 'Vigthoria-v4-Assistant-9B' },
203
331
  ];
204
332
  if (this.hasCloudAccess()) {
205
333
  models.push({ id: 'cloud-fast', name: 'Vigthoria Cloud Fast', description: 'Fast cloud responses for lighter work', tier: 'cloud', backendModel: 'vigthoria-cloud-fast' }, { id: 'cloud-balanced', name: 'Vigthoria Cloud Balanced', description: 'Default quality/cost balance for chat and coding', tier: 'cloud', backendModel: 'vigthoria-cloud-balanced' }, { id: 'cloud-code', name: 'Vigthoria Cloud Code', description: 'Economical cloud coding and completion', tier: 'cloud', backendModel: 'vigthoria-cloud-code' }, { id: 'cloud-power', name: 'Vigthoria Cloud Power', description: 'Premium general intelligence for demanding work', tier: 'cloud', backendModel: 'vigthoria-cloud-power' }, { id: 'cloud-maximum', name: 'Vigthoria Cloud Maximum', description: 'Maximum power for complex architecture and reviews', tier: 'cloud', backendModel: 'vigthoria-cloud-maximum' },
206
334
  // Legacy aliases
207
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' });
208
336
  }
209
- const hubPrefs = this.store.get('hubModelPrefs');
337
+ const hubPrefs = this.get('hubModelPrefs');
210
338
  if (hubPrefs?.enabledCloudModels?.length) {
211
339
  return this.filterModelsByHubPreferences(models, hubPrefs.enabledCloudModels);
212
340
  }
213
341
  return models;
214
342
  }
215
343
  async refreshHubModelPreferences() {
216
- const token = this.store.get('authToken');
217
- const apiUrl = String(this.store.get('apiUrl') || '').replace(/\/$/, '');
344
+ const token = this.get('authToken');
345
+ const apiUrl = String(this.get('apiUrl') || '').replace(/\/$/, '');
218
346
  if (!token || !apiUrl)
219
347
  return;
220
348
  try {
221
- const response = await fetch(`${apiUrl}/api/user/credits/model-preferences`, {
349
+ const response = await guardedFetch(`${apiUrl}/api/user/credits/model-preferences`, {
222
350
  headers: { Authorization: `Bearer ${token}` },
223
351
  signal: AbortSignal.timeout(8000),
224
- });
352
+ }, { audience: 'coder' });
225
353
  if (!response.ok)
226
354
  return;
227
355
  const prefs = await response.json();
228
356
  if (prefs?.success === false)
229
357
  return;
230
- this.store.set('hubModelPrefs', {
358
+ this.set('hubModelPrefs', {
231
359
  enabledCloudModels: Array.isArray(prefs.enabledCloudModels) ? prefs.enabledCloudModels : [],
232
360
  defaultCloudModel: typeof prefs.defaultCloudModel === 'string' ? prefs.defaultCloudModel : undefined,
233
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;