presubmit 0.1.0

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 (220) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +51 -0
  3. package/THIRD_PARTY_NOTICES.md +24 -0
  4. package/apps/web/dist/assets/FileCode-CZZH52o7.js +5 -0
  5. package/apps/web/dist/assets/FileCode-DovDO7Jh.css +1 -0
  6. package/apps/web/dist/assets/SettingsPage-CRvOwdjt.js +4 -0
  7. package/apps/web/dist/assets/SettingsPage-UbYSCwJj.css +1 -0
  8. package/apps/web/dist/assets/TerminalPanel-0sw-J6YS.css +1 -0
  9. package/apps/web/dist/assets/TerminalPanel-BqUjPWav.js +16 -0
  10. package/apps/web/dist/assets/index-BhqNNFja.js +54 -0
  11. package/apps/web/dist/assets/index-DD8VXy8H.css +1 -0
  12. package/apps/web/dist/assets/presubmit-mark-BBQ19Fmr.svg +7 -0
  13. package/apps/web/dist/assets/presubmit-mark-busy-CwBC-7on.svg +8 -0
  14. package/apps/web/dist/assets/react-vendor-wiHys0m2.js +9 -0
  15. package/apps/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
  16. package/apps/web/dist/index.html +6 -0
  17. package/apps/web/dist/service-worker.js +11 -0
  18. package/dist/apps/server/src/agent-builder.js +204 -0
  19. package/dist/apps/server/src/attach.js +163 -0
  20. package/dist/apps/server/src/attachments.js +86 -0
  21. package/dist/apps/server/src/browser-access.js +131 -0
  22. package/dist/apps/server/src/check-provenance.js +95 -0
  23. package/dist/apps/server/src/check-supervisor.js +112 -0
  24. package/dist/apps/server/src/checks.js +402 -0
  25. package/dist/apps/server/src/child-collaboration-guidance.js +2 -0
  26. package/dist/apps/server/src/child-sessions.js +191 -0
  27. package/dist/apps/server/src/cli.js +162 -0
  28. package/dist/apps/server/src/conversation-search.js +12 -0
  29. package/dist/apps/server/src/coordinator-messages.js +72 -0
  30. package/dist/apps/server/src/dev-auth.js +54 -0
  31. package/dist/apps/server/src/dev-server.js +201 -0
  32. package/dist/apps/server/src/dev-worker.js +48 -0
  33. package/dist/apps/server/src/diagnostics.js +105 -0
  34. package/dist/apps/server/src/fast-mode.js +8 -0
  35. package/dist/apps/server/src/folder-picker.js +54 -0
  36. package/dist/apps/server/src/git-blobs.js +62 -0
  37. package/dist/apps/server/src/git-handoff.js +127 -0
  38. package/dist/apps/server/src/history-sources.js +59 -0
  39. package/dist/apps/server/src/hub.js +159 -0
  40. package/dist/apps/server/src/internal-transport.js +38 -0
  41. package/dist/apps/server/src/local-embeddings.js +70 -0
  42. package/dist/apps/server/src/ollama-setup.js +56 -0
  43. package/dist/apps/server/src/planning.js +30 -0
  44. package/dist/apps/server/src/project-agents.js +276 -0
  45. package/dist/apps/server/src/project-memory.js +94 -0
  46. package/dist/apps/server/src/projects.js +106 -0
  47. package/dist/apps/server/src/queued-launch.js +13 -0
  48. package/dist/apps/server/src/reply-search.js +172 -0
  49. package/dist/apps/server/src/review-context.js +60 -0
  50. package/dist/apps/server/src/review-refresh.js +14 -0
  51. package/dist/apps/server/src/reviewers.js +75 -0
  52. package/dist/apps/server/src/search-settings.js +22 -0
  53. package/dist/apps/server/src/semantic-history.js +186 -0
  54. package/dist/apps/server/src/server.js +4338 -0
  55. package/dist/apps/server/src/session-changes.js +181 -0
  56. package/dist/apps/server/src/session-router-settings.js +198 -0
  57. package/dist/apps/server/src/session-workspaces.js +135 -0
  58. package/dist/apps/server/src/sessions.js +579 -0
  59. package/dist/apps/server/src/shared-auth.js +70 -0
  60. package/dist/apps/server/src/shell-evaluator-settings.js +53 -0
  61. package/dist/apps/server/src/store.js +203 -0
  62. package/dist/apps/server/src/tailscale-process.js +115 -0
  63. package/dist/apps/server/src/tailscale-serve-worker.js +33 -0
  64. package/dist/apps/server/src/tailscale-serve.js +276 -0
  65. package/dist/apps/server/src/task-capacity.js +92 -0
  66. package/dist/apps/server/src/task-graph.js +190 -0
  67. package/dist/apps/server/src/task-logs.js +74 -0
  68. package/dist/apps/server/src/task-numbers.js +25 -0
  69. package/dist/apps/server/src/task-orchestrator.js +1088 -0
  70. package/dist/apps/server/src/terminal.js +182 -0
  71. package/dist/apps/server/src/terminals.js +80 -0
  72. package/dist/apps/server/src/tool-safety.js +2 -0
  73. package/dist/apps/server/src/workspace-lock.js +140 -0
  74. package/dist/apps/server/src/workspace-watcher.js +65 -0
  75. package/dist/apps/server/src/workspace.js +517 -0
  76. package/dist/packages/agent-pi/src/agent-builder-tool.js +32 -0
  77. package/dist/packages/agent-pi/src/agent-capabilities.js +44 -0
  78. package/dist/packages/agent-pi/src/ask-user.js +86 -0
  79. package/dist/packages/agent-pi/src/auth.js +510 -0
  80. package/dist/packages/agent-pi/src/check-tools.js +72 -0
  81. package/dist/packages/agent-pi/src/command-policy.js +246 -0
  82. package/dist/packages/agent-pi/src/coordinator-tools.js +122 -0
  83. package/dist/packages/agent-pi/src/fast-mode.js +67 -0
  84. package/dist/packages/agent-pi/src/file-tools.js +163 -0
  85. package/dist/packages/agent-pi/src/history-tool.js +40 -0
  86. package/dist/packages/agent-pi/src/history.js +34 -0
  87. package/dist/packages/agent-pi/src/index.js +177 -0
  88. package/dist/packages/agent-pi/src/ipc.js +314 -0
  89. package/dist/packages/agent-pi/src/jev-session-router.js +408 -0
  90. package/dist/packages/agent-pi/src/jev-tools.js +71 -0
  91. package/dist/packages/agent-pi/src/local-model.js +277 -0
  92. package/dist/packages/agent-pi/src/memory-tools.js +119 -0
  93. package/dist/packages/agent-pi/src/model.js +25 -0
  94. package/dist/packages/agent-pi/src/planning.js +206 -0
  95. package/dist/packages/agent-pi/src/resource-loader.js +23 -0
  96. package/dist/packages/agent-pi/src/review-tools.js +158 -0
  97. package/dist/packages/agent-pi/src/review-transition.js +73 -0
  98. package/dist/packages/agent-pi/src/review.js +40 -0
  99. package/dist/packages/agent-pi/src/routing.js +27 -0
  100. package/dist/packages/agent-pi/src/session-setup.js +118 -0
  101. package/dist/packages/agent-pi/src/shell-context.js +51 -0
  102. package/dist/packages/agent-pi/src/shell-policy.js +281 -0
  103. package/dist/packages/agent-pi/src/shell-safety.js +205 -0
  104. package/dist/packages/agent-pi/src/shell.js +121 -0
  105. package/dist/packages/agent-pi/src/snapshot-permissions.js +12 -0
  106. package/dist/packages/agent-pi/src/spike.js +41 -0
  107. package/dist/packages/agent-pi/src/task-tools.js +53 -0
  108. package/dist/packages/agent-pi/src/types.js +1 -0
  109. package/dist/packages/agent-pi/src/usage.js +30 -0
  110. package/dist/packages/agent-pi/src/worker.js +602 -0
  111. package/dist/packages/protocol/src/activity-summary.js +77 -0
  112. package/dist/packages/protocol/src/agents.js +301 -0
  113. package/dist/packages/protocol/src/browser-access.js +1 -0
  114. package/dist/packages/protocol/src/checks.js +40 -0
  115. package/dist/packages/protocol/src/child-sessions.js +28 -0
  116. package/dist/packages/protocol/src/command-policy.js +3 -0
  117. package/dist/packages/protocol/src/coordinator.js +135 -0
  118. package/dist/packages/protocol/src/delegation.js +40 -0
  119. package/dist/packages/protocol/src/git-diff.js +71 -0
  120. package/dist/packages/protocol/src/git-handoff.js +1 -0
  121. package/dist/packages/protocol/src/index.js +1 -0
  122. package/dist/packages/protocol/src/memory.js +46 -0
  123. package/dist/packages/protocol/src/metrics.js +70 -0
  124. package/dist/packages/protocol/src/planning.js +138 -0
  125. package/dist/packages/protocol/src/retrieval.js +36 -0
  126. package/dist/packages/protocol/src/reviewers.js +17 -0
  127. package/dist/packages/protocol/src/search.js +16 -0
  128. package/dist/packages/protocol/src/session-changes.js +29 -0
  129. package/dist/packages/protocol/src/session-router.js +123 -0
  130. package/dist/packages/protocol/src/shell-evaluator.js +43 -0
  131. package/dist/packages/protocol/src/task-graph.js +1 -0
  132. package/dist/packages/protocol/src/token-usage.js +17 -0
  133. package/dist/packages/protocol/src/user-questions.js +106 -0
  134. package/package.json +71 -0
  135. package/vendor/lancedb/NODEJS_THIRD_PARTY_LICENSES.md +668 -0
  136. package/vendor/lancedb/RUST_THIRD_PARTY_LICENSES.html +14607 -0
  137. package/vendor/lancedb/dist/arrow.d.ts +296 -0
  138. package/vendor/lancedb/dist/arrow.js +1145 -0
  139. package/vendor/lancedb/dist/arrow_type.d.ts +9 -0
  140. package/vendor/lancedb/dist/arrow_type.js +29 -0
  141. package/vendor/lancedb/dist/connection.d.ts +508 -0
  142. package/vendor/lancedb/dist/connection.js +298 -0
  143. package/vendor/lancedb/dist/embedding/embedding_function.d.ts +103 -0
  144. package/vendor/lancedb/dist/embedding/embedding_function.js +192 -0
  145. package/vendor/lancedb/dist/embedding/index.d.ts +37 -0
  146. package/vendor/lancedb/dist/embedding/index.js +108 -0
  147. package/vendor/lancedb/dist/embedding/openai.d.ts +16 -0
  148. package/vendor/lancedb/dist/embedding/openai.js +81 -0
  149. package/vendor/lancedb/dist/embedding/registry.d.ts +94 -0
  150. package/vendor/lancedb/dist/embedding/registry.js +221 -0
  151. package/vendor/lancedb/dist/embedding/transformers.d.ts +36 -0
  152. package/vendor/lancedb/dist/embedding/transformers.js +110 -0
  153. package/vendor/lancedb/dist/header.d.ts +162 -0
  154. package/vendor/lancedb/dist/header.js +217 -0
  155. package/vendor/lancedb/dist/index.d.ts +246 -0
  156. package/vendor/lancedb/dist/index.js +176 -0
  157. package/vendor/lancedb/dist/indices.d.ts +721 -0
  158. package/vendor/lancedb/dist/indices.js +166 -0
  159. package/vendor/lancedb/dist/materialized_view.d.ts +69 -0
  160. package/vendor/lancedb/dist/materialized_view.js +112 -0
  161. package/vendor/lancedb/dist/merge.d.ts +104 -0
  162. package/vendor/lancedb/dist/merge.js +120 -0
  163. package/vendor/lancedb/dist/native.d.ts +1193 -0
  164. package/vendor/lancedb/dist/native.js +688 -0
  165. package/vendor/lancedb/dist/oauth.d.ts +66 -0
  166. package/vendor/lancedb/dist/oauth.js +15 -0
  167. package/vendor/lancedb/dist/otel.d.ts +26 -0
  168. package/vendor/lancedb/dist/otel.js +114 -0
  169. package/vendor/lancedb/dist/permutation.d.ts +143 -0
  170. package/vendor/lancedb/dist/permutation.js +184 -0
  171. package/vendor/lancedb/dist/query.d.ts +663 -0
  172. package/vendor/lancedb/dist/query.js +966 -0
  173. package/vendor/lancedb/dist/rerankers/index.d.ts +5 -0
  174. package/vendor/lancedb/dist/rerankers/index.js +19 -0
  175. package/vendor/lancedb/dist/rerankers/rrf.d.ts +14 -0
  176. package/vendor/lancedb/dist/rerankers/rrf.js +28 -0
  177. package/vendor/lancedb/dist/sanitize.d.ts +32 -0
  178. package/vendor/lancedb/dist/sanitize.js +544 -0
  179. package/vendor/lancedb/dist/scannable.d.ts +92 -0
  180. package/vendor/lancedb/dist/scannable.js +200 -0
  181. package/vendor/lancedb/dist/schema.d.ts +16 -0
  182. package/vendor/lancedb/dist/schema.js +387 -0
  183. package/vendor/lancedb/dist/table.d.ts +1069 -0
  184. package/vendor/lancedb/dist/table.js +498 -0
  185. package/vendor/lancedb/dist/util.d.ts +14 -0
  186. package/vendor/lancedb/dist/util.js +77 -0
  187. package/vendor/lancedb/license_header.txt +2 -0
  188. package/vendor/lancedb/package.json +56 -0
  189. package/vendor/transformers/LICENSE +202 -0
  190. package/vendor/transformers/dist/transformers.cjs +30844 -0
  191. package/vendor/transformers/dist/transformers.mjs +31404 -0
  192. package/vendor/transformers/package.json +55 -0
  193. package/vendor/transformers/types/backends/onnx.d.ts +30 -0
  194. package/vendor/transformers/types/configs.d.ts +85 -0
  195. package/vendor/transformers/types/env.d.ts +110 -0
  196. package/vendor/transformers/types/generation/configuration_utils.d.ts +320 -0
  197. package/vendor/transformers/types/generation/logits_process.d.ts +354 -0
  198. package/vendor/transformers/types/generation/logits_sampler.d.ts +51 -0
  199. package/vendor/transformers/types/generation/parameters.d.ts +47 -0
  200. package/vendor/transformers/types/generation/stopping_criteria.d.ts +81 -0
  201. package/vendor/transformers/types/generation/streamers.d.ts +81 -0
  202. package/vendor/transformers/types/models/whisper/common_whisper.d.ts +8 -0
  203. package/vendor/transformers/types/models/whisper/generation_whisper.d.ts +76 -0
  204. package/vendor/transformers/types/models.d.ts +3684 -0
  205. package/vendor/transformers/types/ops/registry.d.ts +11 -0
  206. package/vendor/transformers/types/pipelines.d.ts +2402 -0
  207. package/vendor/transformers/types/processors.d.ts +924 -0
  208. package/vendor/transformers/types/tokenizers.d.ts +990 -0
  209. package/vendor/transformers/types/transformers.d.ts +13 -0
  210. package/vendor/transformers/types/utils/audio.d.ts +130 -0
  211. package/vendor/transformers/types/utils/constants.d.ts +2 -0
  212. package/vendor/transformers/types/utils/core.d.ts +98 -0
  213. package/vendor/transformers/types/utils/data-structures.d.ts +236 -0
  214. package/vendor/transformers/types/utils/devices.d.ts +18 -0
  215. package/vendor/transformers/types/utils/dtypes.d.ts +19 -0
  216. package/vendor/transformers/types/utils/generic.d.ts +11 -0
  217. package/vendor/transformers/types/utils/hub.d.ts +152 -0
  218. package/vendor/transformers/types/utils/image.d.ts +119 -0
  219. package/vendor/transformers/types/utils/maths.d.ts +280 -0
  220. package/vendor/transformers/types/utils/tensor.d.ts +418 -0
@@ -0,0 +1,181 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { isRecordedFileChange, } from '../../../packages/protocol/src/session-changes.js';
7
+ const exec = promisify(execFile);
8
+ const MAX_BYTES = 20 * 1024 * 1024;
9
+ const MAX_RECORDS = 1000;
10
+ const quotedPath = (path) => (/^[\w./-]+$/.test(path) ? path : JSON.stringify(path));
11
+ async function groupDiff(group, directory) {
12
+ if (group.before === group.after)
13
+ return '';
14
+ const before = join(directory, `${group.id}-before`);
15
+ const after = join(directory, `${group.id}-after`);
16
+ await Promise.all([writeFile(before, group.before ?? ''), writeFile(after, group.after ?? '')]);
17
+ let output;
18
+ try {
19
+ ({ stdout: output } = await exec('git', [
20
+ 'diff',
21
+ '--no-index',
22
+ '--no-ext-diff',
23
+ '--no-textconv',
24
+ '--text',
25
+ '--unified=3',
26
+ '--',
27
+ before,
28
+ after,
29
+ ], {
30
+ cwd: directory,
31
+ maxBuffer: 4 * 1024 * 1024,
32
+ encoding: 'utf8',
33
+ env: {
34
+ ...process.env,
35
+ GIT_CONFIG_NOSYSTEM: '1',
36
+ GIT_CONFIG_GLOBAL: '/dev/null',
37
+ GIT_PAGER: 'cat',
38
+ },
39
+ }));
40
+ }
41
+ catch (error) {
42
+ const result = error;
43
+ if (result.code !== 1 || typeof result.stdout !== 'string')
44
+ throw error;
45
+ output = result.stdout;
46
+ }
47
+ const hunk = output.indexOf('\n@@ ');
48
+ const left = quotedPath('a/' + group.file);
49
+ const right = quotedPath('b/' + group.file);
50
+ return `diff --git ${left} ${right}\n${group.before === null ? 'new file mode 100644\n' : group.after === null ? 'deleted file mode 100644\n' : ''}--- ${group.before === null ? '/dev/null' : left}\n+++ ${group.after === null ? '/dev/null' : right}\n${hunk < 0 ? '' : output.slice(hunk + 1)}`;
51
+ }
52
+ /** Per-session provenance, kept outside AppState and the conversation journal. */
53
+ export class SessionChanges {
54
+ store;
55
+ cached;
56
+ constructor(store) {
57
+ this.store = store;
58
+ store.db.exec(`CREATE TABLE IF NOT EXISTS file_changes (
59
+ id INTEGER PRIMARY KEY AUTOINCREMENT, run_id TEXT NOT NULL, tool_id TEXT NOT NULL,
60
+ write_index INTEGER NOT NULL, file TEXT NOT NULL, before_text TEXT, after_text TEXT,
61
+ omitted TEXT, bytes INTEGER NOT NULL, UNIQUE(run_id, tool_id, write_index)
62
+ );`);
63
+ if (store.get('file-change-tracking') === undefined) {
64
+ store.put('file-change-tracking', {
65
+ earlierHistory: !!store.get('state')?.runs.length,
66
+ });
67
+ }
68
+ }
69
+ /** Call within the event transaction. Duplicate deliveries do not add another edit. */
70
+ record(runId, change) {
71
+ if (!isRecordedFileChange(change))
72
+ throw new Error('Invalid recorded file change');
73
+ if (this.store.db
74
+ .prepare('SELECT 1 FROM file_changes WHERE run_id=? AND tool_id=? AND write_index=?')
75
+ .get(runId, change.toolCallId, change.index))
76
+ return false;
77
+ const usage = this.store.db
78
+ .prepare('SELECT COUNT(*) AS count, COALESCE(SUM(bytes),0) AS bytes FROM file_changes')
79
+ .get();
80
+ if (usage.count >= MAX_RECORDS) {
81
+ if (this.store.get('file-changes-limited'))
82
+ return false;
83
+ this.store.put('file-changes-limited', true);
84
+ this.cached = undefined;
85
+ return true;
86
+ }
87
+ const bytes = change.omitted
88
+ ? 0
89
+ : Buffer.byteLength(change.before ?? '') + Buffer.byteLength(change.after ?? '');
90
+ const omitted = change.omitted ?? (usage.bytes + bytes > MAX_BYTES ? 'storage' : null);
91
+ this.store.db
92
+ .prepare(`INSERT INTO file_changes(run_id,tool_id,write_index,file,before_text,after_text,omitted,bytes) VALUES(?,?,?,?,?,?,?,?)`)
93
+ .run(runId, change.toolCallId, change.index, change.file, omitted ? null : (change.before ?? null), omitted ? null : (change.after ?? null), omitted, omitted ? 0 : bytes);
94
+ this.cached = undefined;
95
+ return true;
96
+ }
97
+ view() {
98
+ const revision = JSON.stringify(this.store.db.prepare('SELECT MAX(id) AS id FROM file_changes').get()) +
99
+ !!this.store.get('file-changes-limited');
100
+ if (this.cached?.revision === revision)
101
+ return this.cached.value;
102
+ const value = this.build();
103
+ this.cached = { revision, value };
104
+ void value.catch(() => {
105
+ if (this.cached?.value === value)
106
+ this.cached = undefined;
107
+ });
108
+ return value;
109
+ }
110
+ async build() {
111
+ const rows = this.store.db
112
+ .prepare('SELECT id,file,before_text,after_text,omitted FROM file_changes ORDER BY id')
113
+ .all();
114
+ const groups = [];
115
+ const last = new Map();
116
+ let partial = !!this.store.get('file-changes-limited');
117
+ const earlierHistory = !!this.store.get('file-change-tracking')
118
+ ?.earlierHistory;
119
+ for (const row of rows) {
120
+ if (row.omitted) {
121
+ partial = true;
122
+ last.delete(row.file);
123
+ continue;
124
+ }
125
+ const previous = last.get(row.file);
126
+ // Intervening edits by another writer must never appear as this session's additions.
127
+ if (previous && previous.after === row.before_text) {
128
+ previous.after = row.after_text;
129
+ previous.edits++;
130
+ }
131
+ else {
132
+ const group = {
133
+ id: row.id,
134
+ file: row.file,
135
+ before: row.before_text,
136
+ after: row.after_text,
137
+ edits: 1,
138
+ };
139
+ groups.push(group);
140
+ last.set(row.file, group);
141
+ }
142
+ }
143
+ const directory = await mkdtemp(join(tmpdir(), 'presubmit-session-diff-'));
144
+ try {
145
+ const diffs = new Map();
146
+ let next = 0;
147
+ const results = await Promise.allSettled(Array.from({ length: Math.min(4, groups.length) }, async () => {
148
+ while (next < groups.length) {
149
+ const group = groups[next++];
150
+ diffs.set(group.id, await groupDiff(group, directory));
151
+ }
152
+ }));
153
+ for (const result of results)
154
+ if (result.status === 'rejected')
155
+ throw result.reason;
156
+ const files = new Map();
157
+ let bytes = 0;
158
+ for (const group of groups) {
159
+ const diff = diffs.get(group.id);
160
+ if (!diff)
161
+ continue;
162
+ bytes += Buffer.byteLength(diff);
163
+ if (bytes > MAX_BYTES) {
164
+ partial = true;
165
+ break;
166
+ }
167
+ const file = files.get(group.file) ?? { path: group.file, groups: [] };
168
+ file.groups.push({ id: group.id, edits: group.edits, diff });
169
+ files.set(group.file, file);
170
+ }
171
+ return {
172
+ files: [...files.values()].sort((a, b) => a.path.localeCompare(b.path)),
173
+ partial,
174
+ earlierHistory,
175
+ };
176
+ }
177
+ finally {
178
+ await rm(directory, { recursive: true, force: true });
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,198 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { defaultSessionRouterSettings, parseSessionRouterSettings, } from '../../../packages/protocol/src/session-router.js';
5
+ const endpoint = 'https://api.typesafe.ai/v1/models';
6
+ const settingsPath = (authPath) => join(dirname(authPath), 'typesafe.json');
7
+ async function read(authPath) {
8
+ try {
9
+ const value = JSON.parse(await readFile(settingsPath(authPath), 'utf8'));
10
+ return {
11
+ version: typeof value.version === 'string' ? value.version : '0',
12
+ apiKey: typeof value.apiKey === 'string' && value.apiKey ? value.apiKey : undefined,
13
+ settings: parseSessionRouterSettings(value.settings ?? defaultSessionRouterSettings),
14
+ models: Array.isArray(value.models)
15
+ ? value.models.map(parseModel).filter((item) => !!item)
16
+ : [],
17
+ };
18
+ }
19
+ catch (error) {
20
+ if (error.code === 'ENOENT')
21
+ return {
22
+ version: '0',
23
+ settings: { ...defaultSessionRouterSettings },
24
+ models: [],
25
+ };
26
+ throw new Error('Could not read TypeSafe session routing settings.');
27
+ }
28
+ }
29
+ function parseModel(value) {
30
+ if (!value || typeof value !== 'object' || Array.isArray(value))
31
+ return undefined;
32
+ const input = value;
33
+ const id = typeof input.id === 'string' ? input.id : input.name;
34
+ if (typeof id !== 'string' || !id.trim())
35
+ return undefined;
36
+ return {
37
+ id: id.trim(),
38
+ ...(typeof input.description === 'string' ? { description: input.description } : {}),
39
+ ...(typeof input.releaseDate === 'string'
40
+ ? { releaseDate: input.releaseDate }
41
+ : typeof input.release_date === 'string'
42
+ ? { releaseDate: input.release_date }
43
+ : {}),
44
+ };
45
+ }
46
+ async function catalog(apiKey, request) {
47
+ const response = await request(endpoint, {
48
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' },
49
+ signal: AbortSignal.timeout(5000),
50
+ });
51
+ if (!response.ok)
52
+ throw new Error(response.status === 401
53
+ ? 'TypeSafe rejected this API key.'
54
+ : `Could not load TypeSafe models (${response.status}).`);
55
+ const value = (await response.json());
56
+ if (!Array.isArray(value.models))
57
+ throw new Error('TypeSafe returned an invalid model catalog.');
58
+ const models = value.models.map(parseModel).filter((item) => !!item);
59
+ if (!models.length)
60
+ throw new Error('No TypeSafe models are available for this account.');
61
+ return models;
62
+ }
63
+ async function write(authPath, saved) {
64
+ const path = settingsPath(authPath);
65
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
66
+ const temp = `${path}.${randomUUID()}.tmp`;
67
+ try {
68
+ await writeFile(temp, JSON.stringify(saved), { mode: 0o600 });
69
+ await rename(temp, path);
70
+ }
71
+ finally {
72
+ await rm(temp, { force: true });
73
+ }
74
+ }
75
+ function publicState(saved) {
76
+ const environment = process.env.TYPESAFE_API_KEY?.trim();
77
+ return {
78
+ version: saved.version,
79
+ settings: saved.settings,
80
+ connected: Boolean(environment || saved.apiKey),
81
+ ...(environment
82
+ ? { source: 'environment' }
83
+ : saved.apiKey
84
+ ? { source: 'saved' }
85
+ : {}),
86
+ models: saved.models,
87
+ ...(environment
88
+ ? { message: 'Connected using TYPESAFE_API_KEY from the server environment.' }
89
+ : {}),
90
+ };
91
+ }
92
+ export async function sessionRouterState(authPath) {
93
+ return publicState(await read(authPath));
94
+ }
95
+ export async function sessionRouterConfig(authPath, request = fetch) {
96
+ try {
97
+ const saved = await read(authPath);
98
+ const apiKey = process.env.TYPESAFE_API_KEY?.trim() || saved.apiKey;
99
+ if (!apiKey ||
100
+ (!saved.settings.enabled &&
101
+ !saved.settings.taskRoutingEnabled &&
102
+ !saved.settings.agentConsultEnabled &&
103
+ !saved.settings.shellSafetyShadowEnabled))
104
+ return undefined;
105
+ return {
106
+ apiKey,
107
+ model: saved.settings.model,
108
+ sessionRoutingEnabled: saved.settings.enabled,
109
+ routeConfidence: saved.settings.routeConfidence,
110
+ taskRoutingEnabled: saved.settings.taskRoutingEnabled,
111
+ taskMinConfidence: saved.settings.taskMinConfidence,
112
+ agentConsultEnabled: saved.settings.agentConsultEnabled,
113
+ agentConsultMinConfidence: saved.settings.agentConsultMinConfidence,
114
+ shellSafetyShadowEnabled: saved.settings.shellSafetyShadowEnabled,
115
+ fetch: request,
116
+ };
117
+ }
118
+ catch {
119
+ return undefined;
120
+ }
121
+ }
122
+ export async function updateSessionRouter(authPath, input, request = fetch) {
123
+ const path = settingsPath(authPath);
124
+ const lock = `${path}.lock`;
125
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
126
+ try {
127
+ await mkdir(lock);
128
+ }
129
+ catch {
130
+ throw new Error('Session routing settings are being saved elsewhere. Reload and retry.');
131
+ }
132
+ try {
133
+ const saved = await read(authPath);
134
+ const action = input.action;
135
+ if (action === 'connect') {
136
+ const apiKey = typeof input.apiKey === 'string' ? input.apiKey.trim() : '';
137
+ if (!apiKey || apiKey.length > 8192 || /[\x00-\x1f\x7f]/.test(apiKey))
138
+ throw new Error('Enter a valid TypeSafe API key.');
139
+ const models = await catalog(apiKey, request);
140
+ const next = { ...saved, version: randomUUID(), apiKey, models };
141
+ await write(authPath, next);
142
+ return publicState(next);
143
+ }
144
+ if (action === 'refresh') {
145
+ const apiKey = process.env.TYPESAFE_API_KEY?.trim() || saved.apiKey;
146
+ if (!apiKey)
147
+ throw new Error('Connect TypeSafe before refreshing models.');
148
+ const next = { ...saved, version: randomUUID(), models: await catalog(apiKey, request) };
149
+ await write(authPath, next);
150
+ return publicState(next);
151
+ }
152
+ if (action === 'save') {
153
+ if (input.version !== saved.version)
154
+ throw new Error('Session routing settings changed. Reload and retry.');
155
+ const settings = parseSessionRouterSettings(input.settings);
156
+ if ((settings.enabled ||
157
+ settings.taskRoutingEnabled ||
158
+ settings.agentConsultEnabled ||
159
+ settings.shellSafetyShadowEnabled) &&
160
+ !process.env.TYPESAFE_API_KEY?.trim() &&
161
+ !saved.apiKey)
162
+ throw new Error('Connect TypeSafe before enabling Jev decisions.');
163
+ const next = { ...saved, version: randomUUID(), settings };
164
+ await write(authPath, next);
165
+ return publicState(next);
166
+ }
167
+ if (action === 'disconnect') {
168
+ const environment = process.env.TYPESAFE_API_KEY?.trim();
169
+ const next = {
170
+ ...saved,
171
+ version: randomUUID(),
172
+ apiKey: undefined,
173
+ settings: environment
174
+ ? saved.settings
175
+ : {
176
+ ...saved.settings,
177
+ enabled: false,
178
+ taskRoutingEnabled: false,
179
+ agentConsultEnabled: false,
180
+ shellSafetyShadowEnabled: false,
181
+ },
182
+ };
183
+ await write(authPath, next);
184
+ return {
185
+ ...publicState(next),
186
+ ...(environment
187
+ ? {
188
+ message: 'Saved credentials removed. TypeSafe remains connected through TYPESAFE_API_KEY; remove it from the environment and restart to disconnect.',
189
+ }
190
+ : {}),
191
+ };
192
+ }
193
+ throw new Error('Invalid session routing settings action.');
194
+ }
195
+ finally {
196
+ await rm(lock, { recursive: true, force: true });
197
+ }
198
+ }
@@ -0,0 +1,135 @@
1
+ import { isGitWorkspace } from './workspace.js';
2
+ import { execFile } from 'node:child_process';
3
+ import { lstat, mkdir, realpath } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ const execute = promisify(execFile);
7
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
8
+ async function git(repository, args, operation) {
9
+ try {
10
+ const result = await execute('git', args, {
11
+ cwd: repository,
12
+ encoding: 'utf8',
13
+ maxBuffer: 64 * 1024,
14
+ timeout: 60_000,
15
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
16
+ });
17
+ return result.stdout.endsWith('\0') ? result.stdout : result.stdout.trim();
18
+ }
19
+ catch (error) {
20
+ // Git stderr can contain hook output and credentials. Keep public errors contextual,
21
+ // but never propagate arbitrary subprocess output to the browser.
22
+ const code = error.code;
23
+ const status = typeof code === 'number' ? ` (exit ${code})` : '';
24
+ throw new Error(`Cannot ${operation}: Git failed${status}. Check the repository and filesystem permissions.`);
25
+ }
26
+ }
27
+ export async function listSessionBranches(repository) {
28
+ if (!(await isGitWorkspace(repository)))
29
+ return { branches: [], current: null, uncommittedFiles: 0 };
30
+ const [refs, current, status] = await Promise.all([
31
+ git(repository, ['for-each-ref', '--format=%(refname:strip=2)', 'refs/heads/'], 'list branches'),
32
+ git(repository, ['rev-parse', '--abbrev-ref', 'HEAD'], 'read current branch'),
33
+ git(repository, ['status', '--porcelain=v1', '-z'], 'inspect uncommitted files'),
34
+ ]);
35
+ const entries = status.split('\0').filter(Boolean);
36
+ let uncommittedFiles = 0;
37
+ for (let i = 0; i < entries.length; i++) {
38
+ uncommittedFiles++;
39
+ if (/^[RC]|^.[RC]/.test(entries[i]))
40
+ i++;
41
+ }
42
+ return {
43
+ branches: refs ? refs.split('\n') : [],
44
+ current: current === 'HEAD' ? null : current,
45
+ uncommittedFiles,
46
+ };
47
+ }
48
+ async function resolveBranch(repository, branch) {
49
+ await git(repository, ['check-ref-format', `refs/heads/${branch}`], 'validate branch name');
50
+ return git(repository, ['rev-parse', '--verify', `refs/heads/${branch}^{commit}`], 'resolve selected branch');
51
+ }
52
+ export async function selectSessionBranch(repository, branch, busy) {
53
+ if (!(await isGitWorkspace(repository)))
54
+ throw new Error('Branch selection requires a Git repository.');
55
+ await resolveBranch(repository, branch);
56
+ const state = await listSessionBranches(repository);
57
+ if (state.current === branch)
58
+ return;
59
+ if (busy())
60
+ throw new Error('Wait for active sessions to finish before switching the shared checkout branch.');
61
+ if (state.uncommittedFiles)
62
+ throw new Error('Commit or stash uncommitted changes before switching the shared checkout branch, or use a worktree.');
63
+ await git(repository, ['switch', '--no-guess', '--', branch], 'switch branch; it may be in use by another worktree');
64
+ }
65
+ export async function createSessionWorkspace(options) {
66
+ if (!(await isGitWorkspace(options.repository)))
67
+ throw new Error('Worktrees require a Git repository. Use Local for this folder.');
68
+ if (!UUID.test(options.id))
69
+ throw new Error('Session ID must be a UUID generated by randomUUID.');
70
+ const branch = `presubmit/${options.id}`;
71
+ await mkdir(options.directory, { recursive: true, mode: 0o700 });
72
+ const workspace = path.join(await realpath(options.directory), options.id);
73
+ try {
74
+ await lstat(workspace);
75
+ throw new Error('Session workspace path already exists. Choose a new session ID.');
76
+ }
77
+ catch (error) {
78
+ if (error.code !== 'ENOENT')
79
+ throw error;
80
+ }
81
+ const existing = await git(options.repository, ['for-each-ref', '--format=%(refname)', `refs/heads/${branch}`], 'inspect session branch');
82
+ if (existing)
83
+ throw new Error('Session branch already exists. Choose a new session ID.');
84
+ if (options.baseCommit && !/^[a-f0-9]{40,64}$/i.test(options.baseCommit))
85
+ throw new Error('Invalid pinned base commit.');
86
+ const commit = options.baseCommit ??
87
+ (options.branch
88
+ ? await resolveBranch(options.repository, options.branch)
89
+ : await git(options.repository, ['rev-parse', '--verify', 'HEAD^{commit}'], 'resolve repository HEAD; create an initial commit first'));
90
+ // -b is intentionally non-forcing: Git also checks collisions atomically. Never
91
+ // delete branches or directories on failure, since they may contain user work.
92
+ await git(options.repository, ['worktree', 'add', '-b', branch, workspace, commit], 'create session worktree');
93
+ return { workspace, branch };
94
+ }
95
+ /** Validate saved sessions on restart. Missing worktrees must never be recreated. */
96
+ export async function validateSessionWorkspace(repository, workspace, branch) {
97
+ if (!branch.startsWith('presubmit/') || !UUID.test(branch.slice('presubmit/'.length)))
98
+ throw new Error('Invalid saved session branch.');
99
+ let canonical;
100
+ try {
101
+ canonical = await realpath(workspace);
102
+ }
103
+ catch {
104
+ throw new Error('Session workspace is missing or inaccessible. Restore its worktree to resume this session.');
105
+ }
106
+ const [root, repositoryCommon, workspaceCommon] = await Promise.all([
107
+ git(canonical, ['rev-parse', '--show-toplevel'], 'locate session worktree'),
108
+ git(repository, ['rev-parse', '--path-format=absolute', '--git-common-dir'], 'identify repository'),
109
+ git(canonical, ['rev-parse', '--path-format=absolute', '--git-common-dir'], 'identify session repository'),
110
+ ]);
111
+ if ((await realpath(root)) !== canonical)
112
+ throw new Error('Saved session workspace is not a worktree root.');
113
+ if ((await realpath(repositoryCommon)) !== (await realpath(workspaceCommon)))
114
+ throw new Error('Saved session workspace belongs to a different repository.');
115
+ // Branch switches and detached HEAD are legitimate user actions in this worktree.
116
+ }
117
+ /** Resolve once at human batch approval; execution uses the immutable object ID. */
118
+ export async function resolveSessionCommit(repository, ref) {
119
+ if (!ref || ref.length > 200 || ref.startsWith('-') || /[\0\r\n]/.test(ref))
120
+ throw new Error('Invalid base ref.');
121
+ return git(repository, ['rev-parse', '--verify', '--end-of-options', `${ref}^{commit}`], 'resolve task base');
122
+ }
123
+ export async function inspectTaskWorkspace(workspace) {
124
+ if (!(await isGitWorkspace(workspace)))
125
+ return {};
126
+ const [commit, status] = await Promise.all([
127
+ resolveSessionCommit(workspace, 'HEAD'),
128
+ git(workspace, ['status', '--porcelain=v1', '-z'], 'inspect task changes'),
129
+ ]);
130
+ return { commit, dirty: !!status };
131
+ }
132
+ export async function commitContains(repository, base, commit) {
133
+ const ancestor = await git(repository, ['merge-base', base, commit], 'check prerequisite integration');
134
+ return ancestor === commit;
135
+ }