@selesai/code 0.1.19 → 0.2.1

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 (157) hide show
  1. package/dist/agents/architect.md +4 -3
  2. package/dist/agents/builder.md +5 -4
  3. package/dist/agents/commentator.md +3 -2
  4. package/dist/agents/explorer.md +4 -4
  5. package/dist/agents/recapper.md +4 -3
  6. package/dist/config.d.ts +6 -7
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +7 -9
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +3 -1
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/compaction/compaction.d.ts.map +1 -1
  14. package/dist/core/compaction/compaction.js +18 -24
  15. package/dist/core/compaction/compaction.js.map +1 -1
  16. package/dist/core/extensions/loader.d.ts.map +1 -1
  17. package/dist/core/extensions/loader.js +2 -0
  18. package/dist/core/extensions/loader.js.map +1 -1
  19. package/dist/core/extensions/types.d.ts +8 -1
  20. package/dist/core/extensions/types.d.ts.map +1 -1
  21. package/dist/core/extensions/types.js.map +1 -1
  22. package/dist/core/skills.d.ts.map +1 -1
  23. package/dist/core/skills.js +25 -11
  24. package/dist/core/skills.js.map +1 -1
  25. package/dist/defaults/models.json +143 -39
  26. package/dist/defaults/settings.json +2 -29
  27. package/dist/extensions/package.json +5 -4
  28. package/dist/extensions/pi-intercom/index.ts +12 -5
  29. package/dist/extensions/pi-powerline-footer/index.ts +4 -1
  30. package/dist/extensions/pi-powerline-footer/tps.ts +286 -0
  31. package/dist/extensions/pi-powerline-footer/welcome.ts +47 -46
  32. package/dist/extensions/pi-subagents/README.md +12 -5
  33. package/dist/extensions/pi-subagents/agents/context-builder.md +2 -2
  34. package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
  35. package/dist/extensions/pi-subagents/package.json +8 -5
  36. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  37. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  38. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +1 -1
  39. package/dist/extensions/pi-subagents/src/agents/agents.ts +3 -3
  40. package/dist/extensions/pi-subagents/src/extension/control-notices.ts +1 -1
  41. package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
  42. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +1 -1
  43. package/dist/extensions/pi-subagents/src/extension/index.ts +29 -2
  44. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +2 -2
  45. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +72 -13
  46. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +1 -1
  47. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +1 -1
  48. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +85 -41
  49. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +1 -1
  50. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +1 -1
  51. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +46 -3
  52. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +2 -2
  53. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +1 -3
  54. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +2 -2
  55. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +18 -18
  56. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +24 -24
  57. package/dist/extensions/pi-subagents/src/runs/shared/spawn-timing.ts +46 -0
  58. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +2 -2
  59. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -4
  60. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +1 -1
  61. package/dist/extensions/pi-subagents/src/shared/types.ts +6 -6
  62. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -3
  63. package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -1
  64. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
  65. package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +1 -1
  66. package/dist/extensions/pi-subagents/src/tui/render.ts +1 -1
  67. package/dist/extensions/pi-subagents/test/unit/spawn-timing.test.ts +45 -0
  68. package/dist/extensions/pi-subagents/tsconfig.runner.json +17 -0
  69. package/dist/extensions/pi-web-agent/package.json +31 -0
  70. package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -0
  71. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
  72. package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -0
  73. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -0
  74. package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -0
  75. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +133 -0
  76. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -0
  77. package/dist/extensions/pi-web-agent/src/extension.ts +126 -0
  78. package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -0
  79. package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -0
  80. package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -0
  81. package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -0
  82. package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -0
  83. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -0
  84. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -0
  85. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -0
  86. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -0
  87. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -0
  88. package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -0
  89. package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -0
  90. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -0
  91. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -0
  92. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -0
  93. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -0
  94. package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -0
  95. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +208 -0
  96. package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -0
  97. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -0
  98. package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -0
  99. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -0
  100. package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -0
  101. package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -0
  102. package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -0
  103. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -0
  104. package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -0
  105. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -0
  106. package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -0
  107. package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -0
  108. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -0
  109. package/dist/extensions/pi-web-agent/src/types.ts +88 -0
  110. package/dist/extensions/ponytail/index.js +189 -0
  111. package/dist/extensions/ponytail/package.json +8 -0
  112. package/dist/extensions/ponytail/ponytail-config.cjs +122 -0
  113. package/dist/extensions/ponytail/ponytail-instructions.cjs +94 -0
  114. package/dist/extensions/ponytail/test/extension.test.js +167 -0
  115. package/dist/extensions/ponytail/test/helpers.test.js +92 -0
  116. package/dist/extensions/question/constants.ts +38 -0
  117. package/dist/extensions/question/dialog-adapter.ts +74 -0
  118. package/dist/extensions/question/helpers.ts +102 -0
  119. package/dist/extensions/question/index.ts +671 -0
  120. package/dist/extensions/question/navigation.ts +14 -0
  121. package/dist/extensions/question/package.json +18 -0
  122. package/dist/extensions/question/question-list.ts +498 -0
  123. package/dist/extensions/question/row-layout.ts +162 -0
  124. package/dist/extensions/question/schemas.ts +69 -0
  125. package/dist/extensions/question/selection-mode.ts +49 -0
  126. package/dist/extensions/question/shortcuts.ts +53 -0
  127. package/dist/extensions/question/tests/helpers.test.ts +178 -0
  128. package/dist/extensions/question/tests/question-list.test.ts +230 -0
  129. package/dist/extensions/question/tests/row-layout.test.ts +136 -0
  130. package/dist/extensions/question/tests/shortcuts.test.ts +85 -0
  131. package/dist/extensions/question/tests/ui-protocol.test.ts +143 -0
  132. package/dist/extensions/question/tui-adapter.ts +33 -0
  133. package/dist/extensions/question/types.ts +102 -0
  134. package/dist/extensions/question/ui-protocol.ts +46 -0
  135. package/dist/extensions/tokenin-onboarding.ts +265 -2
  136. package/dist/extensions/web-agent-onboarding.ts +222 -0
  137. package/dist/extensions/workflow/adapter.ts +588 -0
  138. package/dist/extensions/workflow/extension.ts +24 -0
  139. package/dist/extensions/workflow/modes/prototype.ts +156 -0
  140. package/dist/extensions/workflow/modes/quick.ts +138 -0
  141. package/dist/extensions/workflow/package.json +17 -0
  142. package/dist/extensions/workflow/state-machine.ts +386 -0
  143. package/dist/index.d.ts +1 -1
  144. package/dist/index.d.ts.map +1 -1
  145. package/dist/index.js +1 -1
  146. package/dist/index.js.map +1 -1
  147. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  148. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  149. package/dist/modes/interactive/interactive-mode.js +57 -13
  150. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  151. package/dist/utils/mime.d.ts.map +1 -1
  152. package/dist/utils/mime.js +24 -1
  153. package/dist/utils/mime.js.map +1 -1
  154. package/docs/docs.json +4 -0
  155. package/docs/workflows.md +206 -0
  156. package/package.json +5 -1
  157. package/README.md +0 -198
@@ -1 +1 @@
1
- {"version":3,"file":"mime.d.ts","sourceRoot":"","sources":["../../src/utils/mime.ts"],"names":[],"mappings":"AAKA,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAc9E;AAED,wBAAsB,oCAAoC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CASnG","sourcesContent":["import { open } from \"node:fs/promises\";\n\nconst IMAGE_TYPE_SNIFF_BYTES = 4100;\nconst PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];\n\nexport function detectSupportedImageMimeType(buffer: Uint8Array): string | null {\n\tif (startsWith(buffer, [0xff, 0xd8, 0xff])) {\n\t\treturn buffer[3] === 0xf7 ? null : \"image/jpeg\";\n\t}\n\tif (startsWith(buffer, PNG_SIGNATURE)) {\n\t\treturn isPng(buffer) && !isAnimatedPng(buffer) ? \"image/png\" : null;\n\t}\n\tif (startsWithAscii(buffer, 0, \"GIF\")) {\n\t\treturn \"image/gif\";\n\t}\n\tif (startsWithAscii(buffer, 0, \"RIFF\") && startsWithAscii(buffer, 8, \"WEBP\")) {\n\t\treturn \"image/webp\";\n\t}\n\treturn null;\n}\n\nexport async function detectSupportedImageMimeTypeFromFile(filePath: string): Promise<string | null> {\n\tconst fileHandle = await open(filePath, \"r\");\n\ttry {\n\t\tconst buffer = Buffer.alloc(IMAGE_TYPE_SNIFF_BYTES);\n\t\tconst { bytesRead } = await fileHandle.read(buffer, 0, IMAGE_TYPE_SNIFF_BYTES, 0);\n\t\treturn detectSupportedImageMimeType(buffer.subarray(0, bytesRead));\n\t} finally {\n\t\tawait fileHandle.close();\n\t}\n}\n\nfunction isPng(buffer: Uint8Array): boolean {\n\treturn (\n\t\tbuffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, \"IHDR\")\n\t);\n}\n\nfunction isAnimatedPng(buffer: Uint8Array): boolean {\n\tlet offset = PNG_SIGNATURE.length;\n\twhile (offset + 8 <= buffer.length) {\n\t\tconst chunkLength = readUint32BE(buffer, offset);\n\t\tconst chunkTypeOffset = offset + 4;\n\t\tif (startsWithAscii(buffer, chunkTypeOffset, \"acTL\")) return true;\n\t\tif (startsWithAscii(buffer, chunkTypeOffset, \"IDAT\")) return false;\n\n\t\tconst nextOffset = offset + 8 + chunkLength + 4;\n\t\tif (nextOffset <= offset || nextOffset > buffer.length) return false;\n\t\toffset = nextOffset;\n\t}\n\treturn false;\n}\n\nfunction readUint32BE(buffer: Uint8Array, offset: number): number {\n\treturn (\n\t\t(buffer[offset] ?? 0) * 0x1000000 +\n\t\t((buffer[offset + 1] ?? 0) << 16) +\n\t\t((buffer[offset + 2] ?? 0) << 8) +\n\t\t(buffer[offset + 3] ?? 0)\n\t);\n}\n\nfunction startsWith(buffer: Uint8Array, bytes: number[]): boolean {\n\tif (buffer.length < bytes.length) return false;\n\treturn bytes.every((byte, index) => buffer[index] === byte);\n}\n\nfunction startsWithAscii(buffer: Uint8Array, offset: number, text: string): boolean {\n\tif (buffer.length < offset + text.length) return false;\n\tfor (let index = 0; index < text.length; index++) {\n\t\tif (buffer[offset + index] !== text.charCodeAt(index)) return false;\n\t}\n\treturn true;\n}\n"]}
1
+ {"version":3,"file":"mime.d.ts","sourceRoot":"","sources":["../../src/utils/mime.ts"],"names":[],"mappings":"AAKA,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAc9E;AAiBD,wBAAsB,oCAAoC,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiBnG","sourcesContent":["import { open } from \"node:fs/promises\";\n\nconst IMAGE_TYPE_SNIFF_BYTES = 4100;\nconst PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];\n\nexport function detectSupportedImageMimeType(buffer: Uint8Array): string | null {\n\tif (startsWith(buffer, [0xff, 0xd8, 0xff])) {\n\t\treturn buffer[3] === 0xf7 ? null : \"image/jpeg\";\n\t}\n\tif (startsWith(buffer, PNG_SIGNATURE)) {\n\t\treturn isPng(buffer) && !isAnimatedPng(buffer) ? \"image/png\" : null;\n\t}\n\tif (startsWithAscii(buffer, 0, \"GIF\")) {\n\t\treturn \"image/gif\";\n\t}\n\tif (startsWithAscii(buffer, 0, \"RIFF\") && startsWithAscii(buffer, 8, \"WEBP\")) {\n\t\treturn \"image/webp\";\n\t}\n\treturn null;\n}\n\nconst IMAGE_EXTENSION_MIME_TYPES: Record<string, string> = {\n\tpng: \"image/png\",\n\tjpg: \"image/jpeg\",\n\tjpeg: \"image/jpeg\",\n\tgif: \"image/gif\",\n\twebp: \"image/webp\",\n\tbmp: \"image/bmp\",\n\ttiff: \"image/tiff\",\n\ttif: \"image/tiff\",\n\tico: \"image/x-icon\",\n\theic: \"image/heic\",\n\theif: \"image/heif\",\n\tavif: \"image/avif\",\n};\n\nexport async function detectSupportedImageMimeTypeFromFile(filePath: string): Promise<string | null> {\n\tconst fileHandle = await open(filePath, \"r\");\n\ttry {\n\t\tconst buffer = Buffer.alloc(IMAGE_TYPE_SNIFF_BYTES);\n\t\tconst { bytesRead } = await fileHandle.read(buffer, 0, IMAGE_TYPE_SNIFF_BYTES, 0);\n\t\tconst detected = detectSupportedImageMimeType(buffer.subarray(0, bytesRead));\n\t\tif (detected) return detected;\n\t\t// ponytail: magic-byte sniffer rejects APNG/JPEG2000 and ignores BMP/TIFF/etc.\n\t\t// Fall back to the file extension so image files are routed to the image path\n\t\t// (resize will no-op to an \"omitted\" note) instead of being read as text and\n\t\t// flooding the prompt with base64 garbage. SVG is intentionally excluded: it is\n\t\t// text and is correctly handled by the text branch.\n\t\tconst ext = filePath.toLowerCase().split(\".\").pop();\n\t\treturn ext ? (IMAGE_EXTENSION_MIME_TYPES[ext] ?? null) : null;\n\t} finally {\n\t\tawait fileHandle.close();\n\t}\n}\n\nfunction isPng(buffer: Uint8Array): boolean {\n\treturn (\n\t\tbuffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, \"IHDR\")\n\t);\n}\n\nfunction isAnimatedPng(buffer: Uint8Array): boolean {\n\tlet offset = PNG_SIGNATURE.length;\n\twhile (offset + 8 <= buffer.length) {\n\t\tconst chunkLength = readUint32BE(buffer, offset);\n\t\tconst chunkTypeOffset = offset + 4;\n\t\tif (startsWithAscii(buffer, chunkTypeOffset, \"acTL\")) return true;\n\t\tif (startsWithAscii(buffer, chunkTypeOffset, \"IDAT\")) return false;\n\n\t\tconst nextOffset = offset + 8 + chunkLength + 4;\n\t\tif (nextOffset <= offset || nextOffset > buffer.length) return false;\n\t\toffset = nextOffset;\n\t}\n\treturn false;\n}\n\nfunction readUint32BE(buffer: Uint8Array, offset: number): number {\n\treturn (\n\t\t(buffer[offset] ?? 0) * 0x1000000 +\n\t\t((buffer[offset + 1] ?? 0) << 16) +\n\t\t((buffer[offset + 2] ?? 0) << 8) +\n\t\t(buffer[offset + 3] ?? 0)\n\t);\n}\n\nfunction startsWith(buffer: Uint8Array, bytes: number[]): boolean {\n\tif (buffer.length < bytes.length) return false;\n\treturn bytes.every((byte, index) => buffer[index] === byte);\n}\n\nfunction startsWithAscii(buffer: Uint8Array, offset: number, text: string): boolean {\n\tif (buffer.length < offset + text.length) return false;\n\tfor (let index = 0; index < text.length; index++) {\n\t\tif (buffer[offset + index] !== text.charCodeAt(index)) return false;\n\t}\n\treturn true;\n}\n"]}
@@ -16,12 +16,35 @@ export function detectSupportedImageMimeType(buffer) {
16
16
  }
17
17
  return null;
18
18
  }
19
+ const IMAGE_EXTENSION_MIME_TYPES = {
20
+ png: "image/png",
21
+ jpg: "image/jpeg",
22
+ jpeg: "image/jpeg",
23
+ gif: "image/gif",
24
+ webp: "image/webp",
25
+ bmp: "image/bmp",
26
+ tiff: "image/tiff",
27
+ tif: "image/tiff",
28
+ ico: "image/x-icon",
29
+ heic: "image/heic",
30
+ heif: "image/heif",
31
+ avif: "image/avif",
32
+ };
19
33
  export async function detectSupportedImageMimeTypeFromFile(filePath) {
20
34
  const fileHandle = await open(filePath, "r");
21
35
  try {
22
36
  const buffer = Buffer.alloc(IMAGE_TYPE_SNIFF_BYTES);
23
37
  const { bytesRead } = await fileHandle.read(buffer, 0, IMAGE_TYPE_SNIFF_BYTES, 0);
24
- return detectSupportedImageMimeType(buffer.subarray(0, bytesRead));
38
+ const detected = detectSupportedImageMimeType(buffer.subarray(0, bytesRead));
39
+ if (detected)
40
+ return detected;
41
+ // ponytail: magic-byte sniffer rejects APNG/JPEG2000 and ignores BMP/TIFF/etc.
42
+ // Fall back to the file extension so image files are routed to the image path
43
+ // (resize will no-op to an "omitted" note) instead of being read as text and
44
+ // flooding the prompt with base64 garbage. SVG is intentionally excluded: it is
45
+ // text and is correctly handled by the text branch.
46
+ const ext = filePath.toLowerCase().split(".").pop();
47
+ return ext ? (IMAGE_EXTENSION_MIME_TYPES[ext] ?? null) : null;
25
48
  }
26
49
  finally {
27
50
  await fileHandle.close();
@@ -1 +1 @@
1
- {"version":3,"file":"mime.js","sourceRoot":"","sources":["../../src/utils/mime.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAExC,MAAM,sBAAsB,GAAG,IAAI,CAAC;AACpC,MAAM,aAAa,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAEvE,MAAM,UAAU,4BAA4B,CAAC,MAAkB,EAAiB;IAC/E,IAAI,UAAU,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC;QAC5C,OAAO,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC;IACjD,CAAC;IACD,IAAI,UAAU,CAAC,MAAM,EAAE,aAAa,CAAC,EAAE,CAAC;QACvC,OAAO,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;IACrE,CAAC;IACD,IAAI,eAAe,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,CAAC;QACvC,OAAO,WAAW,CAAC;IACpB,CAAC;IACD,IAAI,eAAe,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,eAAe,CAAC,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC;QAC9E,OAAO,YAAY,CAAC;IACrB,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ;AAED,MAAM,CAAC,KAAK,UAAU,oCAAoC,CAAC,QAAgB,EAA0B;IACpG,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC7C,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;QACpD,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,sBAAsB,EAAE,CAAC,CAAC,CAAC;QAClF,OAAO,4BAA4B,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;IACpE,CAAC;YAAS,CAAC;QACV,MAAM,UAAU,CAAC,KAAK,EAAE,CAAC;IAC1B,CAAC;AAAA,CACD;AAED,SAAS,KAAK,CAAC,MAAkB,EAAW;IAC3C,OAAO,CACN,MAAM,CAAC,MAAM,IAAI,EAAE,IAAI,YAAY,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,eAAe,CAAC,MAAM,EAAE,EAAE,EAAE,MAAM,CAAC,CAC/G,CAAC;AAAA,CACF;AAED,SAAS,aAAa,CAAC,MAAkB,EAAW;IACnD,IAAI,MAAM,GAAG,aAAa,CAAC,MAAM,CAAC;IAClC,OAAO,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;QACpC,MAAM,WAAW,GAAG,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjD,MAAM,eAAe,GAAG,MAAM,GAAG,CAAC,CAAC;QACnC,IAAI,eAAe,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC;YAAE,OAAO,IAAI,CAAC;QAClE,IAAI,eAAe,CAAC,MAAM,EAAE,eAAe,EAAE,MAAM,CAAC;YAAE,OAAO,KAAK,CAAC;QAEnE,MAAM,UAAU,GAAG,MAAM,GAAG,CAAC,GAAG,WAAW,GAAG,CAAC,CAAC;QAChD,IAAI,UAAU,IAAI,MAAM,IAAI,UAAU,GAAG,MAAM,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QACrE,MAAM,GAAG,UAAU,CAAC;IACrB,CAAC;IACD,OAAO,KAAK,CAAC;AAAA,CACb;AAED,SAAS,YAAY,CAAC,MAAkB,EAAE,MAAc,EAAU;IACjE,OAAO,CACN,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,SAAS;QACjC,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;QACjC,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;QAChC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CACzB,CAAC;AAAA,CACF;AAED,SAAS,UAAU,CAAC,MAAkB,EAAE,KAAe,EAAW;IACjE,IAAI,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC/C,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;AAAA,CAC5D;AAED,SAAS,eAAe,CAAC,MAAkB,EAAE,MAAc,EAAE,IAAY,EAAW;IACnF,IAAI,MAAM,CAAC,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACvD,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;QAClD,IAAI,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,KAAK,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;IACrE,CAAC;IACD,OAAO,IAAI,CAAC;AAAA,CACZ","sourcesContent":["import { open } from \"node:fs/promises\";\n\nconst IMAGE_TYPE_SNIFF_BYTES = 4100;\nconst PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];\n\nexport function detectSupportedImageMimeType(buffer: Uint8Array): string | null {\n\tif (startsWith(buffer, [0xff, 0xd8, 0xff])) {\n\t\treturn buffer[3] === 0xf7 ? null : \"image/jpeg\";\n\t}\n\tif (startsWith(buffer, PNG_SIGNATURE)) {\n\t\treturn isPng(buffer) && !isAnimatedPng(buffer) ? \"image/png\" : null;\n\t}\n\tif (startsWithAscii(buffer, 0, \"GIF\")) {\n\t\treturn \"image/gif\";\n\t}\n\tif (startsWithAscii(buffer, 0, \"RIFF\") && startsWithAscii(buffer, 8, \"WEBP\")) {\n\t\treturn \"image/webp\";\n\t}\n\treturn null;\n}\n\nexport async function detectSupportedImageMimeTypeFromFile(filePath: string): Promise<string | null> {\n\tconst fileHandle = await open(filePath, \"r\");\n\ttry {\n\t\tconst buffer = Buffer.alloc(IMAGE_TYPE_SNIFF_BYTES);\n\t\tconst { bytesRead } = await fileHandle.read(buffer, 0, IMAGE_TYPE_SNIFF_BYTES, 0);\n\t\treturn detectSupportedImageMimeType(buffer.subarray(0, bytesRead));\n\t} finally {\n\t\tawait fileHandle.close();\n\t}\n}\n\nfunction isPng(buffer: Uint8Array): boolean {\n\treturn (\n\t\tbuffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, \"IHDR\")\n\t);\n}\n\nfunction isAnimatedPng(buffer: Uint8Array): boolean {\n\tlet offset = PNG_SIGNATURE.length;\n\twhile (offset + 8 <= buffer.length) {\n\t\tconst chunkLength = readUint32BE(buffer, offset);\n\t\tconst chunkTypeOffset = offset + 4;\n\t\tif (startsWithAscii(buffer, chunkTypeOffset, \"acTL\")) return true;\n\t\tif (startsWithAscii(buffer, chunkTypeOffset, \"IDAT\")) return false;\n\n\t\tconst nextOffset = offset + 8 + chunkLength + 4;\n\t\tif (nextOffset <= offset || nextOffset > buffer.length) return false;\n\t\toffset = nextOffset;\n\t}\n\treturn false;\n}\n\nfunction readUint32BE(buffer: Uint8Array, offset: number): number {\n\treturn (\n\t\t(buffer[offset] ?? 0) * 0x1000000 +\n\t\t((buffer[offset + 1] ?? 0) << 16) +\n\t\t((buffer[offset + 2] ?? 0) << 8) +\n\t\t(buffer[offset + 3] ?? 0)\n\t);\n}\n\nfunction startsWith(buffer: Uint8Array, bytes: number[]): boolean {\n\tif (buffer.length < bytes.length) return false;\n\treturn bytes.every((byte, index) => buffer[index] === byte);\n}\n\nfunction startsWithAscii(buffer: Uint8Array, offset: number, text: string): boolean {\n\tif (buffer.length < offset + text.length) return false;\n\tfor (let index = 0; index < text.length; index++) {\n\t\tif (buffer[offset + index] !== text.charCodeAt(index)) return false;\n\t}\n\treturn true;\n}\n"]}
1
+ {"version":3,"file":"mime.js","sourceRoot":"","sources":["../../src/utils/mime.ts"],"names":[],"mappings":"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","sourcesContent":["import { open } from \"node:fs/promises\";\n\nconst IMAGE_TYPE_SNIFF_BYTES = 4100;\nconst PNG_SIGNATURE = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];\n\nexport function detectSupportedImageMimeType(buffer: Uint8Array): string | null {\n\tif (startsWith(buffer, [0xff, 0xd8, 0xff])) {\n\t\treturn buffer[3] === 0xf7 ? null : \"image/jpeg\";\n\t}\n\tif (startsWith(buffer, PNG_SIGNATURE)) {\n\t\treturn isPng(buffer) && !isAnimatedPng(buffer) ? \"image/png\" : null;\n\t}\n\tif (startsWithAscii(buffer, 0, \"GIF\")) {\n\t\treturn \"image/gif\";\n\t}\n\tif (startsWithAscii(buffer, 0, \"RIFF\") && startsWithAscii(buffer, 8, \"WEBP\")) {\n\t\treturn \"image/webp\";\n\t}\n\treturn null;\n}\n\nconst IMAGE_EXTENSION_MIME_TYPES: Record<string, string> = {\n\tpng: \"image/png\",\n\tjpg: \"image/jpeg\",\n\tjpeg: \"image/jpeg\",\n\tgif: \"image/gif\",\n\twebp: \"image/webp\",\n\tbmp: \"image/bmp\",\n\ttiff: \"image/tiff\",\n\ttif: \"image/tiff\",\n\tico: \"image/x-icon\",\n\theic: \"image/heic\",\n\theif: \"image/heif\",\n\tavif: \"image/avif\",\n};\n\nexport async function detectSupportedImageMimeTypeFromFile(filePath: string): Promise<string | null> {\n\tconst fileHandle = await open(filePath, \"r\");\n\ttry {\n\t\tconst buffer = Buffer.alloc(IMAGE_TYPE_SNIFF_BYTES);\n\t\tconst { bytesRead } = await fileHandle.read(buffer, 0, IMAGE_TYPE_SNIFF_BYTES, 0);\n\t\tconst detected = detectSupportedImageMimeType(buffer.subarray(0, bytesRead));\n\t\tif (detected) return detected;\n\t\t// ponytail: magic-byte sniffer rejects APNG/JPEG2000 and ignores BMP/TIFF/etc.\n\t\t// Fall back to the file extension so image files are routed to the image path\n\t\t// (resize will no-op to an \"omitted\" note) instead of being read as text and\n\t\t// flooding the prompt with base64 garbage. SVG is intentionally excluded: it is\n\t\t// text and is correctly handled by the text branch.\n\t\tconst ext = filePath.toLowerCase().split(\".\").pop();\n\t\treturn ext ? (IMAGE_EXTENSION_MIME_TYPES[ext] ?? null) : null;\n\t} finally {\n\t\tawait fileHandle.close();\n\t}\n}\n\nfunction isPng(buffer: Uint8Array): boolean {\n\treturn (\n\t\tbuffer.length >= 16 && readUint32BE(buffer, PNG_SIGNATURE.length) === 13 && startsWithAscii(buffer, 12, \"IHDR\")\n\t);\n}\n\nfunction isAnimatedPng(buffer: Uint8Array): boolean {\n\tlet offset = PNG_SIGNATURE.length;\n\twhile (offset + 8 <= buffer.length) {\n\t\tconst chunkLength = readUint32BE(buffer, offset);\n\t\tconst chunkTypeOffset = offset + 4;\n\t\tif (startsWithAscii(buffer, chunkTypeOffset, \"acTL\")) return true;\n\t\tif (startsWithAscii(buffer, chunkTypeOffset, \"IDAT\")) return false;\n\n\t\tconst nextOffset = offset + 8 + chunkLength + 4;\n\t\tif (nextOffset <= offset || nextOffset > buffer.length) return false;\n\t\toffset = nextOffset;\n\t}\n\treturn false;\n}\n\nfunction readUint32BE(buffer: Uint8Array, offset: number): number {\n\treturn (\n\t\t(buffer[offset] ?? 0) * 0x1000000 +\n\t\t((buffer[offset + 1] ?? 0) << 16) +\n\t\t((buffer[offset + 2] ?? 0) << 8) +\n\t\t(buffer[offset + 3] ?? 0)\n\t);\n}\n\nfunction startsWith(buffer: Uint8Array, bytes: number[]): boolean {\n\tif (buffer.length < bytes.length) return false;\n\treturn bytes.every((byte, index) => buffer[index] === byte);\n}\n\nfunction startsWithAscii(buffer: Uint8Array, offset: number, text: string): boolean {\n\tif (buffer.length < offset + text.length) return false;\n\tfor (let index = 0; index < text.length; index++) {\n\t\tif (buffer[offset + index] !== text.charCodeAt(index)) return false;\n\t}\n\treturn true;\n}\n"]}
package/docs/docs.json CHANGED
@@ -52,6 +52,10 @@
52
52
  "title": "Extensions",
53
53
  "path": "extensions.md"
54
54
  },
55
+ {
56
+ "title": "Workflows",
57
+ "path": "workflows.md"
58
+ },
55
59
  {
56
60
  "title": "Skills",
57
61
  "path": "skills.md"
@@ -0,0 +1,206 @@
1
+ # Workflows
2
+
3
+ Selesai ships a workflow engine under `src/extensions/workflow/`. It powers the built-in `prototype` and `quick` workflows and is designed so you can add a new workflow mode as a thin config file — no engine changes.
4
+
5
+ ## How it fits together
6
+
7
+ ```
8
+ src/extensions/workflow/
9
+ package.json pi package manifest; lists each mode as a pi extension
10
+ state-machine.ts pure phase state machine (no fs, no pi API)
11
+ adapter.ts pi wiring: tools, commands, events, fs, agent continuation
12
+ modes/
13
+ prototype.ts config → export default createWorkflowExtension(config, options)
14
+ quick.ts config → export default createWorkflowExtension(config, options)
15
+ ```
16
+
17
+ - **`state-machine.ts`** is the deep module. It owns the phase graph, artifact gating, skip rules, the terminal close gate, and the reentrancy guard. It imports nothing external — no `node:fs`, no pi API, no `pi-tui`, no `typebox`. Every method returns a `WorkflowEffect` (a discriminated union in domain vocabulary) that the adapter pattern-matches on.
18
+ - **`adapter.ts`** is the thin glue. One state-machine method per call site (`start` tool, `next` tool, `end` tool, `tool_result` auto-advance, `/<command>`) plus one `applyEffect` switch. It owns all pi/fs wiring and the git-based `reuse` skip predicate.
19
+ - **A mode file** is pure data: the phase list, the per-phase artifact filenames, the per-phase prompt generators, the terminal close artifacts, and identity strings (tool names, command name, status key, entry type). Prompts are functions that receive `{ artifactDir, userPrompt }` and return a string.
20
+
21
+ ## To add a future mode
22
+
23
+ Copy `modes/quick.ts` (the smaller one) and change the config. That's the whole change — the engine never needs editing.
24
+
25
+ ### 1. Create the mode file
26
+
27
+ `src/extensions/workflow/modes/rigorous.ts`:
28
+
29
+ ```typescript
30
+ import type {
31
+ Phase,
32
+ PromptContext,
33
+ WorkflowConfig,
34
+ } from "../state-machine.ts";
35
+ import { createWorkflowExtension } from "../adapter.ts";
36
+
37
+ const phases: Phase[] = [
38
+ "grilling",
39
+ "spec", // ← new phase, not in the built-in set
40
+ "research",
41
+ "plan",
42
+ "reuse",
43
+ "handoff",
44
+ "loop",
45
+ "audit",
46
+ "sign-off", // ← new terminal phase
47
+ ];
48
+
49
+ const prompts: Partial<Record<Phase, (ctx: PromptContext) => string>> = {
50
+ grilling: ({ artifactDir, userPrompt }) => `…grilling prompt…`,
51
+ spec: ({ artifactDir }) => `…spec prompt…`,
52
+ research: ({ artifactDir }) => `…research prompt…`,
53
+ plan: ({ artifactDir }) => `…plan prompt…`,
54
+ reuse: ({ artifactDir }) => `…reuse prompt…`,
55
+ handoff: ({ artifactDir }) => `…handoff prompt…`,
56
+ loop: ({ artifactDir }) => `…loop prompt…`,
57
+ audit: ({ artifactDir }) => `…audit prompt…`,
58
+ "sign-off": ({ artifactDir }) => `…sign-off prompt…`,
59
+ };
60
+
61
+ const config: WorkflowConfig = {
62
+ mode: "rigorous",
63
+ phases,
64
+ phaseArtifacts: {
65
+ grilling: "requirements.md",
66
+ spec: "spec.md",
67
+ research: "research.md",
68
+ plan: "plan.md",
69
+ reuse: "reuse.md",
70
+ handoff: "handoff.md",
71
+ loop: "loop-complete.md",
72
+ audit: "review.md",
73
+ "sign-off": "acceptance.md",
74
+ },
75
+ prompts,
76
+ // Files that must exist before end() can close the workflow.
77
+ // Config-owned — declare whatever your terminal phase requires.
78
+ closeArtifacts: ["acceptance.md", "sign-off-report.md"],
79
+ statusKey: "rigorous",
80
+ entryType: "rigorous-phase",
81
+ footerLabel: "rigorous",
82
+ };
83
+
84
+ export default createWorkflowExtension(config, {
85
+ commandName: "rigorous",
86
+ commandDescription:
87
+ "Run the rigorous workflow (grill → spec → research → plan → reuse → handoff → loop → audit → sign-off)",
88
+ toolNames: {
89
+ start: "start_rigorous_workflow",
90
+ next: "next_rigorous_step",
91
+ end: "end_rigorous_workflow",
92
+ },
93
+ toolLabels: {
94
+ start: "Start Rigorous Workflow",
95
+ next: "Next Rigorous Step",
96
+ end: "End Rigorous Workflow",
97
+ },
98
+ });
99
+ ```
100
+
101
+ ### 2. Register it in the package manifest
102
+
103
+ Add the mode to `src/extensions/workflow/package.json` under `pi.extensions`:
104
+
105
+ ```json
106
+ {
107
+ "pi": {
108
+ "extensions": [
109
+ "./modes/prototype.ts",
110
+ "./modes/quick.ts",
111
+ "./modes/rigorous.ts"
112
+ ]
113
+ }
114
+ }
115
+ ```
116
+
117
+ That's it. The loader picks it up at boot; the three tools (`start_rigorous_workflow`, `next_rigorous_step`, `end_rigorous_workflow`) and the `/rigorous` command are registered automatically.
118
+
119
+ ## Config reference
120
+
121
+ | Field | Type | Description |
122
+ |---|---|---|
123
+ | `mode` | `string` | Mode name, echoed in entry payloads and messages. |
124
+ | `phases` | `Phase[]` | Ordered phase list. `Phase` is `string` — new phase names are allowed. |
125
+ | `phaseArtifacts` | `Partial<Record<Phase, string>>` | The artifact file each phase must produce before advancing. Omit a phase to skip its gate. |
126
+ | `prompts` | `Partial<Record<Phase, (ctx) => string>>` | Prompt generator per phase. `ctx = { artifactDir, userPrompt }`. |
127
+ | `closeArtifacts` | `string[]` | Files that must exist before `end()` succeeds. Config-owned, no built-in default. |
128
+ | `skipRules?` | `{ phase, shouldSkip }[]` | Optional per-phase skip rules. `shouldSkip` is a boolean predicate; when true the engine skips to the next phase. Omit to use the adapter's default (skip `reuse` on empty projects). |
129
+ | `statusKey` | `string` | Footer status key. |
130
+ | `entryType` | `string` | Persisted entry custom-type, used for session resume. |
131
+ | `footerLabel` | `string` | Label shown in the footer (`● label · step/total phase`). |
132
+
133
+ ### Adapter options
134
+
135
+ The second argument to `createWorkflowExtension`:
136
+
137
+ | Field | Description |
138
+ |---|---|
139
+ | `toolNames` | `{ start, next, end }` — the three registered tool names. |
140
+ | `toolLabels` | Human-readable labels for the tools. |
141
+ | `commandName` | The `/<command>` name users type to kick off the workflow. |
142
+ | `commandDescription` | Description shown in the command list. |
143
+
144
+ ## How transitions work
145
+
146
+ Every state-machine method returns a `WorkflowEffect` — a discriminated union the adapter switches on:
147
+
148
+ | Effect | Meaning |
149
+ |---|---|
150
+ | `started` | `start()` succeeded; first phase prompt + entry + footer. |
151
+ | `alreadyActive` | `start()` called while a workflow is active. |
152
+ | `advanced` | Phase moved forward (optionally `skipped` a phase). |
153
+ | `blocked` | Current phase's artifact is missing. |
154
+ | `terminalNeedsArtifacts` | At the last phase; a close artifact is missing. |
155
+ | `terminalReady` | At the last phase; all close artifacts present — call `end()`. |
156
+ | `closed` | `end()` succeeded; workflow finished. |
157
+ | `endBlocked` | `end()` called from the wrong phase or with close artifacts missing. |
158
+ | `idle` | No active workflow. |
159
+ | `noOp` | Auto-advance checked, nothing to do (not active, not armed, artifact not present, or already advancing). |
160
+
161
+ The `tool_result` auto-advance hook is one line:
162
+
163
+ ```typescript
164
+ const eff = await sm.onArtifactMaybe(deps);
165
+ applyEffect(pi, ctx, config, eff);
166
+ ```
167
+
168
+ The reentrancy guard lives inside `onArtifactMaybe` — concurrent calls return `noOp`, so a double `write` in one turn cannot double-advance the phase.
169
+
170
+ ## Skip rules
171
+
172
+ By default the adapter skips the `reuse` phase when the project has no git history. To override, supply `skipRules` in your config:
173
+
174
+ ```typescript
175
+ skipRules: [
176
+ { phase: "research", shouldSkip: async () => isWellUnderstoodDomain() },
177
+ { phase: "reuse", shouldSkip: async () => isEmptyProject() },
178
+ ],
179
+ ```
180
+
181
+ `shouldSkip` is a boolean predicate. When it returns `true`, the engine skips to the next phase in `phases` — the mode owns the transition graph, not the adapter.
182
+
183
+ ## Testing a mode
184
+
185
+ The state machine is tested directly with in-memory stubs — no filesystem, no pi mock, no events:
186
+
187
+ ```typescript
188
+ import { WorkflowStateMachine } from "../extensions/workflow/state-machine.ts";
189
+
190
+ const files = new Set<string>();
191
+ const deps = {
192
+ async artifactExists(phase, dir) {
193
+ const file = config.phaseArtifacts[phase];
194
+ return file ? files.has(`${dir}/${file}`) : true;
195
+ },
196
+ async fileExists(path) { return files.has(path); },
197
+ async mkdirArtifactDir() {},
198
+ artifactPathFor: (goal) => `/fake/${goal}`,
199
+ };
200
+
201
+ const sm = new WorkflowStateMachine(config);
202
+ const eff = await sm.start("build X", deps);
203
+ expect(eff.kind).toBe("started");
204
+ ```
205
+
206
+ See `src/__tests__/state-machine.test.ts` for the full set of transition, skip, terminal, rehydrate, and validation tests.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selesai/code",
3
- "version": "0.1.19",
3
+ "version": "0.2.1",
4
4
  "description": "Selesai coding agent",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -38,6 +38,10 @@
38
38
  "chalk": "5.6.2",
39
39
  "cross-spawn": "7.0.6",
40
40
  "diff": "8.0.4",
41
+ "@mozilla/readability": "^0.6.0",
42
+ "cheerio": "^1.1.0",
43
+ "jsdom": "^26.0.0",
44
+ "playwright": "^1.60.0",
41
45
  "glob": "13.0.6",
42
46
  "highlight.js": "10.7.3",
43
47
  "hosted-git-info": "9.0.3",
package/README.md DELETED
@@ -1,198 +0,0 @@
1
- # @selesai/code
2
-
3
- Selesai coding agent — a terminal-based AI coding agent built on the
4
- [`pi`](https://github.com/earendil-works/pi-coding-agent) runtime.
5
-
6
- This repo is the Selesai-branded fork/repackage. It builds the same TypeScript
7
- source and ships a `selesai` CLI binary plus an importable SDK.
8
-
9
- ---
10
-
11
- ## Prerequisites
12
-
13
- - **Node.js ≥ 20** (ESM, `Node16` module resolution)
14
- - **npm ≥ 10**
15
- - A POSIX shell (`shx` handles cross-platform file ops in build scripts)
16
- - No native compilation required for the agent itself; one dependency
17
- (`@silvia-odwyer/photon-node`) ships prebuilt native binaries via npm
18
- optionalDependencies, so just `npm install`.
19
-
20
- ## Install (from source)
21
-
22
- ```bash
23
- git clone git@github.com:SelesaiInTech/selesai-code.git
24
- cd selesai-code
25
- npm install
26
- ```
27
-
28
- ## Build
29
-
30
- ```bash
31
- npm run build
32
- ```
33
-
34
- What it does:
35
-
36
- 1. `tsgo -p tsconfig.build.json` — compiles `src/**/*.ts` → `dist/` with
37
- declarations, source maps, and rewritten `.ts`→`.js` import extensions.
38
- 2. `chmod +x dist/cli.js` — makes the CLI entrypoint executable.
39
- 3. `copy-assets` — copies non-TS assets into `dist/`:
40
- - `src/modes/interactive/theme/*.json`
41
- - `src/modes/interactive/assets/*.png`
42
- - `src/core/export-html/{template.html,template.css,template.js}`
43
- - `src/core/export-html/vendor/*.js`
44
-
45
- Clean rebuild:
46
-
47
- ```bash
48
- npm run clean && npm run build
49
- ```
50
-
51
- ## Run locally
52
-
53
- After building, invoke the CLI directly:
54
-
55
- ```bash
56
- ./dist/cli.js
57
- # or link it globally:
58
- npm link
59
- selesai
60
- ```
61
-
62
- Or run from source without linking (TypeScript via `jiti` is a runtime dep):
63
-
64
- ```bash
65
- node --experimental-strip-types src/cli.ts
66
- ```
67
-
68
- > Note: the `bin` field maps `selesai` → `dist/cli.js`, so the published
69
- > package exposes a `selesai` command once installed.
70
-
71
- ## Project layout
72
-
73
- ```
74
- src/
75
- cli.ts CLI entrypoint
76
- index.ts public SDK exports (main/types)
77
- cli/startup-ui.ts pre-session TUI prompts (first-run setup, selectors)
78
- modes/ interactive, non-interactive, headless modes
79
- core/ agent loop, settings, package manager, export-html
80
- config.ts app name, config dir, paths
81
- docs/ user + contributor documentation
82
- examples/ extension / SDK examples
83
- ```
84
-
85
- Config dir on disk is `~/.selesai` (see `piConfig.configDir` in
86
- `package.json`).
87
-
88
- ---
89
-
90
- ## Publish to npm
91
-
92
- The package is published as **`@selesai/code`** (scoped). There is **no
93
- `prepare`/`prepublishOnly` script** — you must build before publishing so
94
- `dist/` exists and is included by the `files` allowlist.
95
-
96
- ### 1. Authenticate
97
-
98
- ```bash
99
- # one-time: log in to npm under the @selesai scope owner account
100
- npm login
101
- npm whoami # confirm you're authenticated
102
- ```
103
-
104
- > Publishing a scoped package publicly requires the Selesai org (or your
105
- > account) to own the `@selesai` scope. If the scope isn't created yet:
106
- > ```bash
107
- > npm org create selesai <your-npm-username>
108
- > ```
109
-
110
- ### 2. Bump version
111
-
112
- There's no `version` script wired up; use the standard flow:
113
-
114
- ```bash
115
- npm version patch # 0.1.0 -> 0.1.1 (bugfix)
116
- npm version minor # 0.1.0 -> 0.2.0 (backward-compatible feature)
117
- npm version major # 0.1.0 -> 1.0.0 (breaking)
118
- ```
119
-
120
- This updates `package.json`, commits, and tags. Update `CHANGELOG.md`
121
- in the same commit if you maintain one (it's in the `files` allowlist).
122
-
123
- ### 3. Build, verify pack contents, publish
124
-
125
- ```bash
126
- # build dist/ (REQUIRED — npm publishes whatever is on disk)
127
- npm run build
128
-
129
- # dry-run: inspect exactly what gets published (must NOT include src/)
130
- npm pack --dry-run
131
-
132
- # publish to the public registry
133
- npm publish --access public
134
- ```
135
-
136
- `--access public` is mandatory for scoped packages unless you intend a paid
137
- private package; without it the publish fails by default.
138
-
139
- ### 4. Verify
140
-
141
- ```bash
142
- npm view @selesai/code version
143
- # or in a clean sandbox:
144
- npx @selesai/code@latest --help
145
- ```
146
-
147
- ### Continuous publishing (CI)
148
-
149
- Minimal GitHub Actions snippet — adapt to your runner auth (e.g. an
150
- `NPM_TOKEN` secret with publish rights on `@selesai`):
151
-
152
- ```yaml
153
- # .github/workflows/publish.yml
154
- name: publish
155
- on:
156
- release:
157
- types: [published]
158
- jobs:
159
- publish:
160
- runs-on: ubuntu-latest
161
- permissions:
162
- contents: read
163
- steps:
164
- - uses: actions/checkout@v4
165
- - uses: actions/setup-node@v4
166
- with:
167
- node-version: 20
168
- registry-url: https://registry.npmjs.org
169
- - run: npm ci
170
- - run: npm run build
171
- - run: npm publish --access public
172
- env:
173
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
174
- ```
175
-
176
- ### Publishing checklist
177
-
178
- - [ ] `npm run clean && npm run build` succeeds with no errors
179
- - [ ] `npm pack --dry-run` shows only `dist/`, `docs/`, `examples/`,
180
- `CHANGELOG.md`, `package.json`, `README.md`, `LICENSE` — **never `src/`**
181
- - [ ] Version bumped and committed
182
- - [ ] Logged in as an account with publish rights on `@selesai`
183
- - [ ] `--access public` passed (scoped package)
184
- - [ ] `npm view @selesai/code version` matches the version you intended
185
-
186
- ---
187
-
188
- ## Notes & gotchas
189
-
190
- - **No `prepublishOnly` hook.** Forgetting `npm run build` before
191
- `npm publish` will publish a stale (or empty) `dist/`. Always build.
192
- - **`tsgo`** is `@typescript/native-preview` (the Go-backed TS compiler
193
- preview) — it's a devDependency, so CI/`npm ci` installs it automatically.
194
- - **Asset copy is manual**, not a bundler step. If you add new JSON/PNG/HTML
195
- themes or vendor JS, update the `copy-assets` script in `package.json`.
196
- - **Scoped publish.** `@selesai/code` is under a scope; first-time publish
197
- needs `--access public` (public visibility is free, private costs money).
198
- - **Binary name.** Users get a `selesai` command after `npm i -g @selesai/code`.