@ai-devkit/agent-manager 0.38.0 → 0.40.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 (126) hide show
  1. package/dist/__tests__/capacity/devin.test.js +239 -0
  2. package/dist/__tests__/capacity/devin.test.js.map +1 -0
  3. package/dist/__tests__/harnesses/devin/DevinAdapter.test.js +597 -0
  4. package/dist/__tests__/harnesses/devin/DevinAdapter.test.js.map +1 -0
  5. package/dist/__tests__/harnesses/devin/DevinAgentMapper.test.js +91 -0
  6. package/dist/__tests__/harnesses/devin/DevinAgentMapper.test.js.map +1 -0
  7. package/dist/__tests__/harnesses/devin/DevinSessionLocator.test.js +192 -0
  8. package/dist/__tests__/harnesses/devin/DevinSessionLocator.test.js.map +1 -0
  9. package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js +407 -0
  10. package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js.map +1 -0
  11. package/dist/__tests__/harnesses/devin/fixtures.js +95 -0
  12. package/dist/__tests__/harnesses/devin/fixtures.js.map +1 -0
  13. package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js +64 -1
  14. package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js.map +1 -1
  15. package/dist/__tests__/terminal/GhosttyBackend.test.js +334 -0
  16. package/dist/__tests__/terminal/GhosttyBackend.test.js.map +1 -0
  17. package/dist/adapters/AgentAdapter.d.ts +1 -1
  18. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  19. package/dist/adapters/AgentAdapter.js +2 -1
  20. package/dist/adapters/AgentAdapter.js.map +1 -1
  21. package/dist/capacity/index.d.ts +3 -0
  22. package/dist/capacity/index.d.ts.map +1 -1
  23. package/dist/capacity/index.js +8 -0
  24. package/dist/capacity/index.js.map +1 -1
  25. package/dist/capacity/providers/devin.d.ts +13 -0
  26. package/dist/capacity/providers/devin.d.ts.map +1 -0
  27. package/dist/capacity/providers/devin.js +92 -0
  28. package/dist/capacity/providers/devin.js.map +1 -0
  29. package/dist/capacity/sources/devin.d.ts +12 -0
  30. package/dist/capacity/sources/devin.d.ts.map +1 -0
  31. package/dist/capacity/sources/devin.js +29 -0
  32. package/dist/capacity/sources/devin.js.map +1 -0
  33. package/dist/harnesses/devin/DevinAdapter.d.ts +39 -0
  34. package/dist/harnesses/devin/DevinAdapter.d.ts.map +1 -0
  35. package/dist/harnesses/devin/DevinAdapter.js +177 -0
  36. package/dist/harnesses/devin/DevinAdapter.js.map +1 -0
  37. package/dist/harnesses/devin/DevinAgentMapper.d.ts +11 -0
  38. package/dist/harnesses/devin/DevinAgentMapper.d.ts.map +1 -0
  39. package/dist/harnesses/devin/DevinAgentMapper.js +35 -0
  40. package/dist/harnesses/devin/DevinAgentMapper.js.map +1 -0
  41. package/dist/harnesses/devin/DevinSessionLocator.d.ts +46 -0
  42. package/dist/harnesses/devin/DevinSessionLocator.d.ts.map +1 -0
  43. package/dist/harnesses/devin/DevinSessionLocator.js +176 -0
  44. package/dist/harnesses/devin/DevinSessionLocator.js.map +1 -0
  45. package/dist/harnesses/devin/DevinSessionParser.d.ts +29 -0
  46. package/dist/harnesses/devin/DevinSessionParser.d.ts.map +1 -0
  47. package/dist/harnesses/devin/DevinSessionParser.js +166 -0
  48. package/dist/harnesses/devin/DevinSessionParser.js.map +1 -0
  49. package/dist/harnesses/devin/credentials.d.ts +10 -0
  50. package/dist/harnesses/devin/credentials.d.ts.map +1 -0
  51. package/dist/harnesses/devin/credentials.js +66 -0
  52. package/dist/harnesses/devin/credentials.js.map +1 -0
  53. package/dist/harnesses/devin/readiness.d.ts +3 -0
  54. package/dist/harnesses/devin/readiness.d.ts.map +1 -0
  55. package/dist/harnesses/devin/readiness.js +39 -0
  56. package/dist/harnesses/devin/readiness.js.map +1 -0
  57. package/dist/harnesses/index.d.ts.map +1 -1
  58. package/dist/harnesses/index.js +3 -1
  59. package/dist/harnesses/index.js.map +1 -1
  60. package/dist/harnesses/readiness/AgentReadiness.d.ts.map +1 -1
  61. package/dist/harnesses/readiness/AgentReadiness.js +3 -1
  62. package/dist/harnesses/readiness/AgentReadiness.js.map +1 -1
  63. package/dist/harnesses/readiness/types.d.ts +1 -1
  64. package/dist/harnesses/readiness/types.d.ts.map +1 -1
  65. package/dist/harnesses/runtimeProfiles.d.ts.map +1 -1
  66. package/dist/harnesses/runtimeProfiles.js +2 -1
  67. package/dist/harnesses/runtimeProfiles.js.map +1 -1
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +1 -1
  71. package/dist/index.js.map +1 -1
  72. package/dist/terminal/TtyWriter.d.ts.map +1 -1
  73. package/dist/terminal/TtyWriter.js +4 -3
  74. package/dist/terminal/TtyWriter.js.map +1 -1
  75. package/dist/terminal/backends/ghostty.d.ts +3 -0
  76. package/dist/terminal/backends/ghostty.d.ts.map +1 -0
  77. package/dist/terminal/backends/ghostty.js +161 -0
  78. package/dist/terminal/backends/ghostty.js.map +1 -0
  79. package/dist/terminal/backends/index.d.ts.map +1 -1
  80. package/dist/terminal/backends/index.js +2 -0
  81. package/dist/terminal/backends/index.js.map +1 -1
  82. package/dist/terminal/backends/shared.d.ts +2 -0
  83. package/dist/terminal/backends/shared.d.ts.map +1 -1
  84. package/dist/terminal/backends/shared.js +4 -1
  85. package/dist/terminal/backends/shared.js.map +1 -1
  86. package/dist/terminal/types.d.ts +1 -0
  87. package/dist/terminal/types.d.ts.map +1 -1
  88. package/dist/terminal/types.js +1 -0
  89. package/dist/terminal/types.js.map +1 -1
  90. package/dist/utils/process.d.ts +2 -0
  91. package/dist/utils/process.d.ts.map +1 -1
  92. package/dist/utils/process.js +3 -0
  93. package/dist/utils/process.js.map +1 -1
  94. package/package.json +1 -1
  95. package/src/__tests__/capacity/devin.test.ts +210 -0
  96. package/src/__tests__/capacity/fixtures/devin-config.json +6 -0
  97. package/src/__tests__/capacity/fixtures/devin-credentials.toml +4 -0
  98. package/src/__tests__/capacity/fixtures/devin-quota-nonplan.json +5 -0
  99. package/src/__tests__/capacity/fixtures/devin-quota.json +10 -0
  100. package/src/__tests__/harnesses/devin/DevinAdapter.test.ts +292 -0
  101. package/src/__tests__/harnesses/devin/DevinAgentMapper.test.ts +88 -0
  102. package/src/__tests__/harnesses/devin/DevinSessionLocator.test.ts +139 -0
  103. package/src/__tests__/harnesses/devin/DevinSessionParser.test.ts +218 -0
  104. package/src/__tests__/harnesses/devin/fixtures.ts +144 -0
  105. package/src/__tests__/harnesses/readiness/AgentReadiness.test.ts +41 -0
  106. package/src/__tests__/terminal/GhosttyBackend.test.ts +271 -0
  107. package/src/adapters/AgentAdapter.ts +1 -0
  108. package/src/capacity/index.ts +12 -0
  109. package/src/capacity/providers/devin.ts +121 -0
  110. package/src/capacity/sources/devin.ts +49 -0
  111. package/src/harnesses/devin/DevinAdapter.ts +224 -0
  112. package/src/harnesses/devin/DevinAgentMapper.ts +43 -0
  113. package/src/harnesses/devin/DevinSessionLocator.ts +230 -0
  114. package/src/harnesses/devin/DevinSessionParser.ts +236 -0
  115. package/src/harnesses/devin/credentials.ts +88 -0
  116. package/src/harnesses/devin/readiness.ts +36 -0
  117. package/src/harnesses/index.ts +2 -0
  118. package/src/harnesses/readiness/AgentReadiness.ts +2 -0
  119. package/src/harnesses/runtimeProfiles.ts +1 -0
  120. package/src/index.ts +1 -0
  121. package/src/terminal/TtyWriter.ts +5 -5
  122. package/src/terminal/backends/ghostty.ts +180 -0
  123. package/src/terminal/backends/index.ts +2 -0
  124. package/src/terminal/backends/shared.ts +6 -1
  125. package/src/terminal/types.ts +1 -0
  126. package/src/utils/process.ts +5 -0
@@ -0,0 +1,407 @@
1
+ import Database from "better-sqlite3";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import { DevinSessionParser } from "../../../harnesses/devin/DevinSessionParser.js";
7
+ import { writeDatabase } from "./fixtures.js";
8
+ describe("DevinSessionParser", ()=>{
9
+ let tmpDir;
10
+ let dbPath;
11
+ let parser;
12
+ beforeEach(()=>{
13
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "devin-parser-test-"));
14
+ dbPath = path.join(tmpDir, "sessions.db");
15
+ parser = new DevinSessionParser();
16
+ });
17
+ afterEach(()=>{
18
+ fs.rmSync(tmpDir, {
19
+ recursive: true,
20
+ force: true
21
+ });
22
+ });
23
+ function openDb() {
24
+ return new Database(dbPath, {
25
+ readonly: true
26
+ });
27
+ }
28
+ it("returns user and assistant messages in node order, skipping system", ()=>{
29
+ writeDatabase(dbPath, {
30
+ nodes: [
31
+ {
32
+ sessionId: "s",
33
+ nodeId: 1,
34
+ role: "system",
35
+ content: "preamble"
36
+ },
37
+ {
38
+ sessionId: "s",
39
+ nodeId: 2,
40
+ role: "user",
41
+ content: "hello",
42
+ isUserInput: true
43
+ },
44
+ {
45
+ sessionId: "s",
46
+ nodeId: 3,
47
+ role: "assistant",
48
+ content: "hi"
49
+ },
50
+ // Later turn roots a fresh preamble chain:
51
+ {
52
+ sessionId: "s",
53
+ nodeId: 4,
54
+ role: "system",
55
+ content: "more preamble"
56
+ },
57
+ {
58
+ sessionId: "s",
59
+ nodeId: 5,
60
+ role: "user",
61
+ content: "again",
62
+ isUserInput: true
63
+ },
64
+ {
65
+ sessionId: "s",
66
+ nodeId: 6,
67
+ role: "assistant",
68
+ content: "done"
69
+ }
70
+ ]
71
+ });
72
+ const db = openDb();
73
+ try {
74
+ expect(parser.getConversation(db, "s")).toEqual([
75
+ {
76
+ role: "user",
77
+ content: "hello"
78
+ },
79
+ {
80
+ role: "assistant",
81
+ content: "hi"
82
+ },
83
+ {
84
+ role: "user",
85
+ content: "again"
86
+ },
87
+ {
88
+ role: "assistant",
89
+ content: "done"
90
+ }
91
+ ]);
92
+ } finally{
93
+ db.close();
94
+ }
95
+ });
96
+ it("dedupes sibling revisions by message_id keeping the highest node_id", ()=>{
97
+ writeDatabase(dbPath, {
98
+ nodes: [
99
+ {
100
+ sessionId: "s",
101
+ nodeId: 1,
102
+ role: "assistant",
103
+ content: "draft",
104
+ messageId: "m1"
105
+ },
106
+ {
107
+ sessionId: "s",
108
+ nodeId: 2,
109
+ role: "assistant",
110
+ content: "final",
111
+ messageId: "m1"
112
+ }
113
+ ]
114
+ });
115
+ const db = openDb();
116
+ try {
117
+ expect(parser.getConversation(db, "s")).toEqual([
118
+ {
119
+ role: "assistant",
120
+ content: "final"
121
+ }
122
+ ]);
123
+ } finally{
124
+ db.close();
125
+ }
126
+ });
127
+ it("skips non-input user nodes and empty content", ()=>{
128
+ writeDatabase(dbPath, {
129
+ nodes: [
130
+ {
131
+ sessionId: "s",
132
+ nodeId: 1,
133
+ role: "user",
134
+ content: "wrapper",
135
+ isUserInput: false
136
+ },
137
+ {
138
+ sessionId: "s",
139
+ nodeId: 2,
140
+ role: "user",
141
+ content: " ",
142
+ isUserInput: true
143
+ },
144
+ {
145
+ sessionId: "s",
146
+ nodeId: 3,
147
+ role: "user",
148
+ content: "real",
149
+ isUserInput: true
150
+ }
151
+ ]
152
+ });
153
+ const db = openDb();
154
+ try {
155
+ expect(parser.getConversation(db, "s")).toEqual([
156
+ {
157
+ role: "user",
158
+ content: "real"
159
+ }
160
+ ]);
161
+ } finally{
162
+ db.close();
163
+ }
164
+ });
165
+ it("includes tools and thinking only in verbose mode", ()=>{
166
+ writeDatabase(dbPath, {
167
+ nodes: [
168
+ {
169
+ sessionId: "s",
170
+ nodeId: 1,
171
+ role: "assistant",
172
+ content: "working",
173
+ thinking: "hmm",
174
+ toolCalls: [
175
+ {
176
+ function: {
177
+ name: "exec"
178
+ }
179
+ }
180
+ ]
181
+ },
182
+ {
183
+ sessionId: "s",
184
+ nodeId: 2,
185
+ role: "tool",
186
+ content: "tool output",
187
+ name: "exec"
188
+ }
189
+ ]
190
+ });
191
+ const db = openDb();
192
+ try {
193
+ expect(parser.getConversation(db, "s")).toEqual([
194
+ {
195
+ role: "assistant",
196
+ content: "working"
197
+ }
198
+ ]);
199
+ expect(parser.getConversation(db, "s", {
200
+ verbose: true
201
+ })).toEqual([
202
+ {
203
+ role: "assistant",
204
+ content: "[thinking] hmm"
205
+ },
206
+ {
207
+ role: "assistant",
208
+ content: "working"
209
+ },
210
+ {
211
+ role: "assistant",
212
+ content: "[tool: exec]"
213
+ },
214
+ {
215
+ role: "assistant",
216
+ content: "[tool: exec] tool output"
217
+ }
218
+ ]);
219
+ } finally{
220
+ db.close();
221
+ }
222
+ });
223
+ it("honors tail and bounds the query window", ()=>{
224
+ writeDatabase(dbPath, {
225
+ nodes: Array.from({
226
+ length: 30
227
+ }, (_, i)=>({
228
+ sessionId: "s",
229
+ nodeId: i + 1,
230
+ role: "user",
231
+ content: `msg-${i + 1}`,
232
+ isUserInput: true
233
+ }))
234
+ });
235
+ const db = openDb();
236
+ try {
237
+ const convo = parser.getConversation(db, "s", {
238
+ tail: 3
239
+ });
240
+ expect(convo).toEqual([
241
+ {
242
+ role: "user",
243
+ content: "msg-28"
244
+ },
245
+ {
246
+ role: "user",
247
+ content: "msg-29"
248
+ },
249
+ {
250
+ role: "user",
251
+ content: "msg-30"
252
+ }
253
+ ]);
254
+ } finally{
255
+ db.close();
256
+ }
257
+ });
258
+ it("skips malformed chat_message rows and missing sessions", ()=>{
259
+ writeDatabase(dbPath, {
260
+ nodes: [
261
+ {
262
+ sessionId: "s",
263
+ nodeId: 1,
264
+ raw: "{not json"
265
+ },
266
+ {
267
+ sessionId: "s",
268
+ nodeId: 2,
269
+ role: "assistant",
270
+ content: "ok"
271
+ }
272
+ ]
273
+ });
274
+ const db = openDb();
275
+ try {
276
+ expect(parser.getConversation(db, "s")).toEqual([
277
+ {
278
+ role: "assistant",
279
+ content: "ok"
280
+ }
281
+ ]);
282
+ expect(parser.getConversation(db, "missing")).toEqual([]);
283
+ } finally{
284
+ db.close();
285
+ }
286
+ });
287
+ it("reports session stats from the frontier node and latest prompt", ()=>{
288
+ writeDatabase(dbPath, {
289
+ nodes: [
290
+ {
291
+ sessionId: "s",
292
+ nodeId: 1,
293
+ role: "user",
294
+ content: "go",
295
+ createdAt: 100
296
+ },
297
+ {
298
+ sessionId: "s",
299
+ nodeId: 2,
300
+ role: "assistant",
301
+ content: "done",
302
+ createdAt: 200
303
+ }
304
+ ],
305
+ prompts: [
306
+ {
307
+ sessionId: "s",
308
+ content: "first task",
309
+ timestamp: 100
310
+ },
311
+ {
312
+ sessionId: "s",
313
+ content: "/mcp",
314
+ timestamp: 300
315
+ },
316
+ {
317
+ sessionId: "s",
318
+ content: "latest task",
319
+ timestamp: 200
320
+ },
321
+ {
322
+ sessionId: "s",
323
+ content: "!shell",
324
+ timestamp: 400,
325
+ isShell: 1
326
+ }
327
+ ]
328
+ });
329
+ const db = openDb();
330
+ try {
331
+ expect(parser.getSessionStats(db, "s")).toEqual({
332
+ lastRole: "assistant",
333
+ lastTimeUpdated: 200_000,
334
+ summary: "latest task"
335
+ });
336
+ expect(parser.getSessionStats(db, "missing")).toEqual({
337
+ lastRole: null,
338
+ lastTimeUpdated: 0,
339
+ summary: ""
340
+ });
341
+ } finally{
342
+ db.close();
343
+ }
344
+ });
345
+ it("falls back to the first is_user_input node for the summary", ()=>{
346
+ writeDatabase(dbPath, {
347
+ nodes: [
348
+ {
349
+ sessionId: "s",
350
+ nodeId: 1,
351
+ role: "user",
352
+ content: "wrapper",
353
+ isUserInput: false
354
+ },
355
+ {
356
+ sessionId: "s",
357
+ nodeId: 2,
358
+ role: "user",
359
+ content: "real first",
360
+ isUserInput: true
361
+ }
362
+ ]
363
+ });
364
+ const db = openDb();
365
+ try {
366
+ expect(parser.getFirstUserMessage(db, "s")).toBe("real first");
367
+ } finally{
368
+ db.close();
369
+ }
370
+ });
371
+ it("falls back to the last is_user_input node for the working-on summary", ()=>{
372
+ writeDatabase(dbPath, {
373
+ nodes: [
374
+ {
375
+ sessionId: "s",
376
+ nodeId: 1,
377
+ role: "user",
378
+ content: "first",
379
+ isUserInput: true
380
+ },
381
+ {
382
+ sessionId: "s",
383
+ nodeId: 2,
384
+ role: "user",
385
+ content: "wrapper",
386
+ isUserInput: false
387
+ },
388
+ {
389
+ sessionId: "s",
390
+ nodeId: 3,
391
+ role: "user",
392
+ content: "last real",
393
+ isUserInput: true
394
+ }
395
+ ]
396
+ });
397
+ const db = openDb();
398
+ try {
399
+ expect(parser.getLastUserPrompt(db, "s")).toBe("last real");
400
+ expect(parser.getLastUserPrompt(db, "missing")).toBe("");
401
+ } finally{
402
+ db.close();
403
+ }
404
+ });
405
+ });
406
+
407
+ //# sourceMappingURL=DevinSessionParser.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/__tests__/harnesses/devin/DevinSessionParser.test.ts"],"sourcesContent":["import Database from \"better-sqlite3\";\nimport * as fs from \"fs\";\nimport * as os from \"os\";\nimport * as path from \"path\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\nimport { DevinSessionParser } from \"../../../harnesses/devin/DevinSessionParser.js\";\nimport { writeDatabase } from \"./fixtures.js\";\n\ndescribe(\"DevinSessionParser\", () => {\n let tmpDir: string;\n let dbPath: string;\n let parser: DevinSessionParser;\n\n beforeEach(() => {\n tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), \"devin-parser-test-\"));\n dbPath = path.join(tmpDir, \"sessions.db\");\n parser = new DevinSessionParser();\n });\n\n afterEach(() => {\n fs.rmSync(tmpDir, { recursive: true, force: true });\n });\n\n function openDb(): Database.Database {\n return new Database(dbPath, { readonly: true });\n }\n\n it(\"returns user and assistant messages in node order, skipping system\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, role: \"system\", content: \"preamble\" },\n { sessionId: \"s\", nodeId: 2, role: \"user\", content: \"hello\", isUserInput: true },\n { sessionId: \"s\", nodeId: 3, role: \"assistant\", content: \"hi\" },\n // Later turn roots a fresh preamble chain:\n { sessionId: \"s\", nodeId: 4, role: \"system\", content: \"more preamble\" },\n { sessionId: \"s\", nodeId: 5, role: \"user\", content: \"again\", isUserInput: true },\n { sessionId: \"s\", nodeId: 6, role: \"assistant\", content: \"done\" },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getConversation(db, \"s\")).toEqual([\n { role: \"user\", content: \"hello\" },\n { role: \"assistant\", content: \"hi\" },\n { role: \"user\", content: \"again\" },\n { role: \"assistant\", content: \"done\" },\n ]);\n } finally {\n db.close();\n }\n });\n\n it(\"dedupes sibling revisions by message_id keeping the highest node_id\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, role: \"assistant\", content: \"draft\", messageId: \"m1\" },\n { sessionId: \"s\", nodeId: 2, role: \"assistant\", content: \"final\", messageId: \"m1\" },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getConversation(db, \"s\")).toEqual([\n { role: \"assistant\", content: \"final\" },\n ]);\n } finally {\n db.close();\n }\n });\n\n it(\"skips non-input user nodes and empty content\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, role: \"user\", content: \"wrapper\", isUserInput: false },\n { sessionId: \"s\", nodeId: 2, role: \"user\", content: \" \", isUserInput: true },\n { sessionId: \"s\", nodeId: 3, role: \"user\", content: \"real\", isUserInput: true },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getConversation(db, \"s\")).toEqual([{ role: \"user\", content: \"real\" }]);\n } finally {\n db.close();\n }\n });\n\n it(\"includes tools and thinking only in verbose mode\", () => {\n writeDatabase(dbPath, {\n nodes: [\n {\n sessionId: \"s\",\n nodeId: 1,\n role: \"assistant\",\n content: \"working\",\n thinking: \"hmm\",\n toolCalls: [{ function: { name: \"exec\" } }],\n },\n { sessionId: \"s\", nodeId: 2, role: \"tool\", content: \"tool output\", name: \"exec\" },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getConversation(db, \"s\")).toEqual([\n { role: \"assistant\", content: \"working\" },\n ]);\n expect(parser.getConversation(db, \"s\", { verbose: true })).toEqual([\n { role: \"assistant\", content: \"[thinking] hmm\" },\n { role: \"assistant\", content: \"working\" },\n { role: \"assistant\", content: \"[tool: exec]\" },\n { role: \"assistant\", content: \"[tool: exec] tool output\" },\n ]);\n } finally {\n db.close();\n }\n });\n\n it(\"honors tail and bounds the query window\", () => {\n writeDatabase(dbPath, {\n nodes: Array.from({ length: 30 }, (_, i) => ({\n sessionId: \"s\",\n nodeId: i + 1,\n role: \"user\" as const,\n content: `msg-${i + 1}`,\n isUserInput: true,\n })),\n });\n const db = openDb();\n try {\n const convo = parser.getConversation(db, \"s\", { tail: 3 });\n expect(convo).toEqual([\n { role: \"user\", content: \"msg-28\" },\n { role: \"user\", content: \"msg-29\" },\n { role: \"user\", content: \"msg-30\" },\n ]);\n } finally {\n db.close();\n }\n });\n\n it(\"skips malformed chat_message rows and missing sessions\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, raw: \"{not json\" },\n { sessionId: \"s\", nodeId: 2, role: \"assistant\", content: \"ok\" },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getConversation(db, \"s\")).toEqual([\n { role: \"assistant\", content: \"ok\" },\n ]);\n expect(parser.getConversation(db, \"missing\")).toEqual([]);\n } finally {\n db.close();\n }\n });\n\n it(\"reports session stats from the frontier node and latest prompt\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, role: \"user\", content: \"go\", createdAt: 100 },\n { sessionId: \"s\", nodeId: 2, role: \"assistant\", content: \"done\", createdAt: 200 },\n ],\n prompts: [\n { sessionId: \"s\", content: \"first task\", timestamp: 100 },\n { sessionId: \"s\", content: \"/mcp\", timestamp: 300 },\n { sessionId: \"s\", content: \"latest task\", timestamp: 200 },\n { sessionId: \"s\", content: \"!shell\", timestamp: 400, isShell: 1 },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getSessionStats(db, \"s\")).toEqual({\n lastRole: \"assistant\",\n lastTimeUpdated: 200_000,\n summary: \"latest task\",\n });\n expect(parser.getSessionStats(db, \"missing\")).toEqual({\n lastRole: null,\n lastTimeUpdated: 0,\n summary: \"\",\n });\n } finally {\n db.close();\n }\n });\n\n it(\"falls back to the first is_user_input node for the summary\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, role: \"user\", content: \"wrapper\", isUserInput: false },\n { sessionId: \"s\", nodeId: 2, role: \"user\", content: \"real first\", isUserInput: true },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getFirstUserMessage(db, \"s\")).toBe(\"real first\");\n } finally {\n db.close();\n }\n });\n\n it(\"falls back to the last is_user_input node for the working-on summary\", () => {\n writeDatabase(dbPath, {\n nodes: [\n { sessionId: \"s\", nodeId: 1, role: \"user\", content: \"first\", isUserInput: true },\n { sessionId: \"s\", nodeId: 2, role: \"user\", content: \"wrapper\", isUserInput: false },\n { sessionId: \"s\", nodeId: 3, role: \"user\", content: \"last real\", isUserInput: true },\n ],\n });\n const db = openDb();\n try {\n expect(parser.getLastUserPrompt(db, \"s\")).toBe(\"last real\");\n expect(parser.getLastUserPrompt(db, \"missing\")).toBe(\"\");\n } finally {\n db.close();\n }\n });\n});\n"],"names":["Database","fs","os","path","afterEach","beforeEach","describe","expect","it","DevinSessionParser","writeDatabase","tmpDir","dbPath","parser","mkdtempSync","join","tmpdir","rmSync","recursive","force","openDb","readonly","nodes","sessionId","nodeId","role","content","isUserInput","db","getConversation","toEqual","close","messageId","thinking","toolCalls","function","name","verbose","Array","from","length","_","i","convo","tail","raw","createdAt","prompts","timestamp","isShell","getSessionStats","lastRole","lastTimeUpdated","summary","getFirstUserMessage","toBe","getLastUserPrompt"],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ import Database from "better-sqlite3";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ export function writeDatabase(dbPath, data) {
5
+ fs.mkdirSync(path.dirname(dbPath), {
6
+ recursive: true
7
+ });
8
+ const db = new Database(dbPath);
9
+ try {
10
+ db.exec(`
11
+ CREATE TABLE IF NOT EXISTS sessions (
12
+ id TEXT PRIMARY KEY,
13
+ working_directory TEXT NOT NULL,
14
+ backend_type TEXT NOT NULL DEFAULT 'local',
15
+ model TEXT NOT NULL DEFAULT 'test-model',
16
+ agent_mode TEXT NOT NULL DEFAULT 'normal',
17
+ created_at INTEGER NOT NULL,
18
+ last_activity_at INTEGER NOT NULL,
19
+ title TEXT,
20
+ main_chain_id INTEGER,
21
+ hidden INTEGER NOT NULL DEFAULT 0
22
+ );
23
+ CREATE TABLE IF NOT EXISTS message_nodes (
24
+ row_id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ session_id TEXT NOT NULL,
26
+ node_id INTEGER NOT NULL,
27
+ parent_node_id INTEGER,
28
+ chat_message TEXT NOT NULL,
29
+ created_at INTEGER NOT NULL,
30
+ UNIQUE(session_id, node_id)
31
+ );
32
+ CREATE TABLE IF NOT EXISTS prompt_history (
33
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
34
+ content TEXT NOT NULL,
35
+ timestamp INTEGER NOT NULL,
36
+ session_id TEXT NOT NULL,
37
+ is_shell INTEGER NOT NULL DEFAULT 0
38
+ );
39
+ `);
40
+ const insertSession = db.prepare(`
41
+ INSERT INTO sessions (id, working_directory, created_at, last_activity_at, title, main_chain_id, hidden)
42
+ VALUES (?, ?, ?, ?, ?, ?, ?)
43
+ `);
44
+ for (const session of data.sessions ?? []){
45
+ insertSession.run(session.id, session.directory, session.timeCreated, session.lastActivityAt ?? session.timeCreated, session.title ?? null, session.mainChainId ?? null, session.hidden ?? 0);
46
+ }
47
+ const insertNode = db.prepare(`
48
+ INSERT INTO message_nodes (session_id, node_id, parent_node_id, chat_message, created_at)
49
+ VALUES (?, ?, ?, ?, ?)
50
+ `);
51
+ for (const node of data.nodes ?? []){
52
+ const chatMessage = node.raw ?? JSON.stringify({
53
+ message_id: node.messageId ?? `msg-${node.nodeId}`,
54
+ role: node.role,
55
+ content: node.content ?? "",
56
+ ...node.toolCalls ? {
57
+ tool_calls: node.toolCalls
58
+ } : {},
59
+ ...node.thinking ? {
60
+ thinking: {
61
+ thinking: node.thinking
62
+ }
63
+ } : {},
64
+ ...node.name ? {
65
+ name: node.name
66
+ } : {},
67
+ ...node.isUserInput !== undefined ? {
68
+ metadata: {
69
+ is_user_input: node.isUserInput
70
+ }
71
+ } : {}
72
+ });
73
+ insertNode.run(node.sessionId, node.nodeId, node.parentNodeId ?? null, chatMessage, node.createdAt ?? 1000 + node.nodeId);
74
+ }
75
+ const insertPrompt = db.prepare(`
76
+ INSERT INTO prompt_history (session_id, content, timestamp, is_shell)
77
+ VALUES (?, ?, ?, ?)
78
+ `);
79
+ for (const prompt of data.prompts ?? []){
80
+ insertPrompt.run(prompt.sessionId, prompt.content, prompt.timestamp ?? 1000, prompt.isShell ?? 0);
81
+ }
82
+ } finally{
83
+ db.close();
84
+ }
85
+ }
86
+ export function writeLocks(locksDir, locks) {
87
+ fs.mkdirSync(locksDir, {
88
+ recursive: true
89
+ });
90
+ for (const [sessionId, pid] of Object.entries(locks)){
91
+ fs.writeFileSync(path.join(locksDir, `${sessionId}.lock`), String(pid));
92
+ }
93
+ }
94
+
95
+ //# sourceMappingURL=fixtures.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/__tests__/harnesses/devin/fixtures.ts"],"sourcesContent":["import Database from \"better-sqlite3\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\nexport interface DevinSessionInput {\n id: string;\n directory: string;\n title?: string | null;\n timeCreated: number;\n lastActivityAt?: number;\n mainChainId?: number | null;\n hidden?: number;\n}\n\nexport interface DevinNodeInput {\n nodeId: number;\n parentNodeId?: number | null;\n role: string;\n content?: string;\n messageId?: string;\n createdAt?: number;\n isUserInput?: boolean;\n toolCalls?: { name?: string; function?: { name?: string } }[];\n thinking?: string;\n name?: string;\n /** Raw chat_message text; when set, other fields are ignored. */\n raw?: string;\n}\n\nexport interface DevinPromptInput {\n sessionId: string;\n content: string;\n timestamp?: number;\n isShell?: number;\n}\n\nexport function writeDatabase(\n dbPath: string,\n data: {\n sessions?: DevinSessionInput[];\n nodes?: (DevinNodeInput & { sessionId: string })[];\n prompts?: DevinPromptInput[];\n },\n): void {\n fs.mkdirSync(path.dirname(dbPath), { recursive: true });\n const db = new Database(dbPath);\n try {\n db.exec(`\n CREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n working_directory TEXT NOT NULL,\n backend_type TEXT NOT NULL DEFAULT 'local',\n model TEXT NOT NULL DEFAULT 'test-model',\n agent_mode TEXT NOT NULL DEFAULT 'normal',\n created_at INTEGER NOT NULL,\n last_activity_at INTEGER NOT NULL,\n title TEXT,\n main_chain_id INTEGER,\n hidden INTEGER NOT NULL DEFAULT 0\n );\n CREATE TABLE IF NOT EXISTS message_nodes (\n row_id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n node_id INTEGER NOT NULL,\n parent_node_id INTEGER,\n chat_message TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n UNIQUE(session_id, node_id)\n );\n CREATE TABLE IF NOT EXISTS prompt_history (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n content TEXT NOT NULL,\n timestamp INTEGER NOT NULL,\n session_id TEXT NOT NULL,\n is_shell INTEGER NOT NULL DEFAULT 0\n );\n `);\n\n const insertSession = db.prepare(`\n INSERT INTO sessions (id, working_directory, created_at, last_activity_at, title, main_chain_id, hidden)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n `);\n for (const session of data.sessions ?? []) {\n insertSession.run(\n session.id,\n session.directory,\n session.timeCreated,\n session.lastActivityAt ?? session.timeCreated,\n session.title ?? null,\n session.mainChainId ?? null,\n session.hidden ?? 0,\n );\n }\n\n const insertNode = db.prepare(`\n INSERT INTO message_nodes (session_id, node_id, parent_node_id, chat_message, created_at)\n VALUES (?, ?, ?, ?, ?)\n `);\n for (const node of data.nodes ?? []) {\n const chatMessage =\n node.raw ??\n JSON.stringify({\n message_id: node.messageId ?? `msg-${node.nodeId}`,\n role: node.role,\n content: node.content ?? \"\",\n ...(node.toolCalls ? { tool_calls: node.toolCalls } : {}),\n ...(node.thinking ? { thinking: { thinking: node.thinking } } : {}),\n ...(node.name ? { name: node.name } : {}),\n ...(node.isUserInput !== undefined\n ? { metadata: { is_user_input: node.isUserInput } }\n : {}),\n });\n insertNode.run(\n node.sessionId,\n node.nodeId,\n node.parentNodeId ?? null,\n chatMessage,\n node.createdAt ?? 1000 + node.nodeId,\n );\n }\n\n const insertPrompt = db.prepare(`\n INSERT INTO prompt_history (session_id, content, timestamp, is_shell)\n VALUES (?, ?, ?, ?)\n `);\n for (const prompt of data.prompts ?? []) {\n insertPrompt.run(\n prompt.sessionId,\n prompt.content,\n prompt.timestamp ?? 1000,\n prompt.isShell ?? 0,\n );\n }\n } finally {\n db.close();\n }\n}\n\nexport function writeLocks(locksDir: string, locks: Record<string, number | string>): void {\n fs.mkdirSync(locksDir, { recursive: true });\n for (const [sessionId, pid] of Object.entries(locks)) {\n fs.writeFileSync(path.join(locksDir, `${sessionId}.lock`), String(pid));\n }\n}\n"],"names":["Database","fs","path","writeDatabase","dbPath","data","mkdirSync","dirname","recursive","db","exec","insertSession","prepare","session","sessions","run","id","directory","timeCreated","lastActivityAt","title","mainChainId","hidden","insertNode","node","nodes","chatMessage","raw","JSON","stringify","message_id","messageId","nodeId","role","content","toolCalls","tool_calls","thinking","name","isUserInput","undefined","metadata","is_user_input","sessionId","parentNodeId","createdAt","insertPrompt","prompt","prompts","timestamp","isShell","close","writeLocks","locksDir","locks","pid","Object","entries","writeFileSync","join","String"],"mappings":"AAAA,OAAOA,cAAc,iBAAiB;AACtC,YAAYC,QAAQ,KAAK;AACzB,YAAYC,UAAU,OAAO;AAkC7B,OAAO,SAASC,cACdC,MAAc,EACdC,IAIC;IAEDJ,GAAGK,SAAS,CAACJ,KAAKK,OAAO,CAACH,SAAS;QAAEI,WAAW;IAAK;IACrD,MAAMC,KAAK,IAAIT,SAASI;IACxB,IAAI;QACFK,GAAGC,IAAI,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA6BT,CAAC;QAED,MAAMC,gBAAgBF,GAAGG,OAAO,CAAC,CAAC;;;IAGlC,CAAC;QACD,KAAK,MAAMC,WAAWR,KAAKS,QAAQ,IAAI,EAAE,CAAE;YACzCH,cAAcI,GAAG,CACfF,QAAQG,EAAE,EACVH,QAAQI,SAAS,EACjBJ,QAAQK,WAAW,EACnBL,QAAQM,cAAc,IAAIN,QAAQK,WAAW,EAC7CL,QAAQO,KAAK,IAAI,MACjBP,QAAQQ,WAAW,IAAI,MACvBR,QAAQS,MAAM,IAAI;QAEtB;QAEA,MAAMC,aAAad,GAAGG,OAAO,CAAC,CAAC;;;IAG/B,CAAC;QACD,KAAK,MAAMY,QAAQnB,KAAKoB,KAAK,IAAI,EAAE,CAAE;YACnC,MAAMC,cACJF,KAAKG,GAAG,IACRC,KAAKC,SAAS,CAAC;gBACbC,YAAYN,KAAKO,SAAS,IAAI,CAAC,IAAI,EAAEP,KAAKQ,MAAM,EAAE;gBAClDC,MAAMT,KAAKS,IAAI;gBACfC,SAASV,KAAKU,OAAO,IAAI;gBACzB,GAAIV,KAAKW,SAAS,GAAG;oBAAEC,YAAYZ,KAAKW,SAAS;gBAAC,IAAI,CAAC,CAAC;gBACxD,GAAIX,KAAKa,QAAQ,GAAG;oBAAEA,UAAU;wBAAEA,UAAUb,KAAKa,QAAQ;oBAAC;gBAAE,IAAI,CAAC,CAAC;gBAClE,GAAIb,KAAKc,IAAI,GAAG;oBAAEA,MAAMd,KAAKc,IAAI;gBAAC,IAAI,CAAC,CAAC;gBACxC,GAAId,KAAKe,WAAW,KAAKC,YACrB;oBAAEC,UAAU;wBAAEC,eAAelB,KAAKe,WAAW;oBAAC;gBAAE,IAChD,CAAC,CAAC;YACR;YACFhB,WAAWR,GAAG,CACZS,KAAKmB,SAAS,EACdnB,KAAKQ,MAAM,EACXR,KAAKoB,YAAY,IAAI,MACrBlB,aACAF,KAAKqB,SAAS,IAAI,OAAOrB,KAAKQ,MAAM;QAExC;QAEA,MAAMc,eAAerC,GAAGG,OAAO,CAAC,CAAC;;;IAGjC,CAAC;QACD,KAAK,MAAMmC,UAAU1C,KAAK2C,OAAO,IAAI,EAAE,CAAE;YACvCF,aAAa/B,GAAG,CACdgC,OAAOJ,SAAS,EAChBI,OAAOb,OAAO,EACda,OAAOE,SAAS,IAAI,MACpBF,OAAOG,OAAO,IAAI;QAEtB;IACF,SAAU;QACRzC,GAAG0C,KAAK;IACV;AACF;AAEA,OAAO,SAASC,WAAWC,QAAgB,EAAEC,KAAsC;IACjFrD,GAAGK,SAAS,CAAC+C,UAAU;QAAE7C,WAAW;IAAK;IACzC,KAAK,MAAM,CAACmC,WAAWY,IAAI,IAAIC,OAAOC,OAAO,CAACH,OAAQ;QACpDrD,GAAGyD,aAAa,CAACxD,KAAKyD,IAAI,CAACN,UAAU,GAAGV,UAAU,KAAK,CAAC,GAAGiB,OAAOL;IACpE;AACF"}
@@ -82,6 +82,7 @@ function fixture(overrides = {}) {
82
82
  ".kiro",
83
83
  ".gemini/antigravity-cli",
84
84
  ".config/opencode",
85
+ ".config/devin",
85
86
  ".pi"
86
87
  ]){
87
88
  files[path.join(homeDir, directory)] = "<dir>";
@@ -125,6 +126,10 @@ function fixture(overrides = {}) {
125
126
  stdout: "● litellm api\n● OpenAI oauth\n",
126
127
  stderr: ""
127
128
  };
129
+ if (command === "devin") return {
130
+ stdout: "Logged in (via Devin).\n\nUser:\n Name: test-user\n",
131
+ stderr: ""
132
+ };
128
133
  throw new Error(`unexpected command ${command}`);
129
134
  }),
130
135
  codexAuth: async ()=>true,
@@ -148,7 +153,8 @@ describe("agent readiness", ()=>{
148
153
  "antigravity_cli",
149
154
  "opencode",
150
155
  "copilot",
151
- "pi"
156
+ "pi",
157
+ "devin"
152
158
  ]);
153
159
  for (const [agent, report] of Object.entries(reports)){
154
160
  expect(report.type).toBe(agent);
@@ -337,6 +343,63 @@ describe("agent readiness", ()=>{
337
343
  status: "pass"
338
344
  });
339
345
  });
346
+ it("checks Devin auth from devin auth status output", async ()=>{
347
+ const { options } = fixture({
348
+ runCommand: vi.fn(async (command)=>{
349
+ if (command === "devin") return {
350
+ stdout: "Logged in (via Devin).\n\nUser:\n Name: test-user\n",
351
+ stderr: ""
352
+ };
353
+ if (command === "pi") return {
354
+ stdout: "npm:@ai-devkit/pi-session-tracker\n",
355
+ stderr: ""
356
+ };
357
+ if (command === "claude") return {
358
+ stdout: JSON.stringify({
359
+ loggedIn: true
360
+ }),
361
+ stderr: ""
362
+ };
363
+ throw new Error(`unexpected command ${command}`);
364
+ })
365
+ });
366
+ const report = await getAgentReadinessReport("devin", options);
367
+ expect(report.auth).toMatchObject({
368
+ state: "authenticated",
369
+ source: "devin auth status",
370
+ provider: null,
371
+ availableProviders: [
372
+ "devin"
373
+ ],
374
+ status: "pass"
375
+ });
376
+ });
377
+ it("reports Devin as unauthenticated without a login line", async ()=>{
378
+ const { options } = fixture({
379
+ runCommand: vi.fn(async (command)=>{
380
+ if (command === "devin") return {
381
+ stdout: "Not logged in\n",
382
+ stderr: ""
383
+ };
384
+ if (command === "pi") return {
385
+ stdout: "npm:@ai-devkit/pi-session-tracker\n",
386
+ stderr: ""
387
+ };
388
+ if (command === "claude") return {
389
+ stdout: JSON.stringify({
390
+ loggedIn: true
391
+ }),
392
+ stderr: ""
393
+ };
394
+ throw new Error(`unexpected command ${command}`);
395
+ })
396
+ });
397
+ const report = await getAgentReadinessReport("devin", options);
398
+ expect(report.auth).toMatchObject({
399
+ state: "unauthenticated",
400
+ status: "fail"
401
+ });
402
+ });
340
403
  });
341
404
 
342
405
  //# sourceMappingURL=AgentReadiness.test.js.map