@code-yeongyu/senpi 2026.9.29-5 → 2026.9.30

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 (140) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/bundle/chunks/{anthropic-messages-HEVKB3PL.js → anthropic-messages-ADSIWAJ5.js} +1 -1
  3. package/dist/bundle/chunks/{app-server-command-32AFWZEH.js → app-server-command-B3ZYM2WN.js} +1 -1
  4. package/dist/bundle/chunks/{chunk-2WAKOPNA.js → chunk-4P4DUW66.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-7RVWDN5Z.js → chunk-6GV3L3GZ.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-OUG6SDT7.js → chunk-6JZQLUAJ.js} +1 -1
  7. package/dist/bundle/chunks/{chunk-MJQWYN2W.js → chunk-7MKMDWHC.js} +2 -2
  8. package/dist/bundle/chunks/{chunk-XBGO2JZD.js → chunk-A7AQSA6R.js} +1 -1
  9. package/dist/bundle/chunks/{chunk-BJICJY47.js → chunk-DVVROXLZ.js} +1 -1
  10. package/dist/bundle/chunks/chunk-EIG6K5U3.js +6 -0
  11. package/dist/bundle/chunks/chunk-FAWC2EIR.js +2 -0
  12. package/dist/bundle/chunks/{chunk-WCAKUCN3.js → chunk-FNCILR4E.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-XHEMQA2T.js → chunk-HPESFBMB.js} +2 -2
  14. package/dist/bundle/chunks/{chunk-BBNWT72C.js → chunk-HVBFDRP4.js} +1 -1
  15. package/dist/bundle/chunks/{chunk-63QHOG5A.js → chunk-I77ENHCQ.js} +81 -74
  16. package/dist/bundle/chunks/{chunk-UMLU3I2G.js → chunk-ITXF6FAM.js} +1 -1
  17. package/dist/bundle/chunks/{chunk-77AUUH3Z.js → chunk-IXONH7ML.js} +2 -2
  18. package/dist/bundle/chunks/{chunk-K2SHQNAA.js → chunk-JX2CSKLG.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-4VWWEMCJ.js → chunk-JXZ5LDJP.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-VLTMFAWQ.js → chunk-KWI3KUF2.js} +1 -1
  21. package/dist/bundle/chunks/{chunk-7CQX3QFV.js → chunk-L4QGZJKQ.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-AGVPQLIU.js → chunk-MDXUQDGF.js} +1 -1
  23. package/dist/bundle/chunks/{chunk-2N3BZ5DV.js → chunk-OOA6OL42.js} +1 -1
  24. package/dist/bundle/chunks/{chunk-2POUJCWM.js → chunk-OVGH7VP3.js} +1 -1
  25. package/dist/bundle/chunks/{chunk-ZDNR3YVJ.js → chunk-RMVQ5YRX.js} +1 -1
  26. package/dist/bundle/chunks/{chunk-SH6VUPE4.js → chunk-T5MOQDGL.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-CNYWF72S.js → chunk-VKECIIYV.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-U7BNOE23.js → chunk-VNYUGCC7.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-ZPMJIPQC.js → chunk-YSNNL5HU.js} +1 -1
  30. package/dist/bundle/chunks/cli-main-F6WNYPUT.js +3 -0
  31. package/dist/bundle/chunks/{help-fast-path-ZUNFOE53.js → help-fast-path-34Z6I2PH.js} +1 -1
  32. package/dist/bundle/chunks/{host-command-V2LDNNXB.js → host-command-PUOIPEBI.js} +1 -1
  33. package/dist/bundle/chunks/{host-lifecycle-PFLJRXEN.js → host-lifecycle-4VJUHV5U.js} +1 -1
  34. package/dist/bundle/chunks/host-lifecycle.js +1 -1
  35. package/dist/bundle/chunks/{interactive-mode-J6KGDRR7.js → interactive-mode-KM454JPC.js} +1 -1
  36. package/dist/bundle/chunks/{list-tips-ASETMEFS.js → list-tips-GWRRD5UC.js} +1 -1
  37. package/dist/bundle/chunks/{multi-session-host-IBVJ3WN7.js → multi-session-host-ZFT6RM2S.js} +2 -2
  38. package/dist/bundle/chunks/{native-loader-WM7NDPEO.js → native-loader-HW7GQYPY.js} +1 -1
  39. package/dist/bundle/chunks/{openai-completions-SA2UJW7Q.js → openai-completions-32RTG3Q4.js} +1 -1
  40. package/dist/bundle/chunks/{package-manager-cli-JJ5R7UWV.js → package-manager-cli-LNQOWI3A.js} +1 -1
  41. package/dist/bundle/chunks/{rg-engine-G2YTYL2T.js → rg-engine-LWZA7TRH.js} +1 -1
  42. package/dist/bundle/chunks/{rotation-stream-EX7PWQ4S.js → rotation-stream-DIKK7U57.js} +1 -1
  43. package/dist/bundle/chunks/{rpc-mode-ITND2BVC.js → rpc-mode-H7QGQI4P.js} +1 -1
  44. package/dist/bundle/chunks/{schedule-command-D6JNRP3D.js → schedule-command-BUFJOM5L.js} +1 -1
  45. package/dist/bundle/chunks/session-control-endpoint-QIIA6MFR.js +3 -0
  46. package/dist/bundle/chunks/{session-picker-K5FIKV6C.js → session-picker-P53MTWTS.js} +1 -1
  47. package/dist/bundle/chunks/session-worker.js +110 -103
  48. package/dist/bundle/chunks/{state-store-IIQL55GC.js → state-store-QGLITAI4.js} +1 -1
  49. package/dist/bundle/cli.js +2 -2
  50. package/dist/bundle/index.js +1 -1
  51. package/dist/bundle/rpc-entry.js +1 -1
  52. package/dist/bundle/runtime-manifest.json +1 -1
  53. package/dist/cli.js +1 -1
  54. package/dist/core/dynamic-prompt/build.d.ts +4 -3
  55. package/dist/core/dynamic-prompt/build.js +4 -2
  56. package/dist/core/dynamic-prompt/handoff.d.ts +9 -1
  57. package/dist/core/dynamic-prompt/handoff.js +11 -1
  58. package/dist/core/dynamic-prompt/intent-gate.js +1 -1
  59. package/dist/core/dynamic-prompt/style.js +4 -2
  60. package/dist/core/dynamic-prompt/types.d.ts +7 -1
  61. package/dist/core/dynamic-prompt/types.js +3 -1
  62. package/dist/core/dynamic-prompt/verification.d.ts +1 -1
  63. package/dist/core/dynamic-prompt/verification.js +2 -1
  64. package/dist/core/extensions/builtin/anthropic-subscription/stream-protocol.d.ts +1 -1
  65. package/dist/core/extensions/builtin/anthropic-subscription/tools.d.ts +9 -2
  66. package/dist/core/extensions/builtin/anthropic-subscription/tools.js +9 -4
  67. package/dist/core/extensions/builtin/ask-user/resume.js +20 -9
  68. package/dist/core/extensions/builtin/ask-user/tool.d.ts +9 -0
  69. package/dist/core/extensions/builtin/ask-user/tool.js +1 -1
  70. package/dist/core/extensions/builtin/cache-keepalive/index.js +17 -4
  71. package/dist/core/extensions/builtin/chat-reply-scrub/index.d.ts +16 -0
  72. package/dist/core/extensions/builtin/chat-reply-scrub/index.js +39 -0
  73. package/dist/core/extensions/builtin/chat-reply-scrub/scaffold-strip.d.ts +7 -0
  74. package/dist/core/extensions/builtin/chat-reply-scrub/scaffold-strip.js +125 -0
  75. package/dist/core/extensions/builtin/index.js +3 -0
  76. package/dist/core/extensions/builtin/permission-system/cli.js +1 -0
  77. package/dist/core/extensions/builtin/permission-system/config.d.ts +1 -0
  78. package/dist/core/extensions/builtin/permission-system/config.js +10 -0
  79. package/dist/core/extensions/builtin/permission-system/index.js +2 -2
  80. package/dist/core/extensions/builtin/permission-system/settings.js +1 -1
  81. package/dist/core/extensions/builtin/permission-system/types.d.ts +1 -1
  82. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5-1.js +5 -5
  83. package/dist/core/extensions/builtin/prompt-preset/claude-fable-5.js +6 -6
  84. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5-5.js +4 -4
  85. package/dist/core/extensions/builtin/prompt-preset/claude-opus-5.js +4 -4
  86. package/dist/core/extensions/builtin/prompt-preset/claude-sonnet-5-5.js +4 -4
  87. package/dist/core/extensions/builtin/prompt-preset/gpt-5.5.js +9 -6
  88. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +10 -7
  89. package/dist/core/extensions/builtin/prompt-preset/gpt-6-astra.js +15 -6
  90. package/dist/core/extensions/builtin/prompt-preset/gpt-surface.d.ts +2 -2
  91. package/dist/core/extensions/builtin/prompt-preset/grok-4.5.js +4 -4
  92. package/dist/core/extensions/builtin/prompt-preset/grok-4.6.js +4 -4
  93. package/dist/core/extensions/builtin/prompt-preset/grok-4.7.js +3 -3
  94. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-6.js +1 -1
  95. package/dist/core/extensions/builtin/prompt-preset/kimi-k2-code.js +1 -1
  96. package/dist/core/extensions/builtin/prompt-preset/kimi-k3.js +6 -6
  97. package/dist/core/extensions/builtin/recommended-models/index.d.ts +1 -1
  98. package/dist/core/extensions/builtin/recommended-models/index.js +1 -1
  99. package/dist/core/extensions/builtin/todotools/first-turn.d.ts +6 -4
  100. package/dist/core/extensions/builtin/todotools/first-turn.js +17 -5
  101. package/dist/core/extensions/builtin/todotools/todo-ask.d.ts +1 -0
  102. package/dist/core/extensions/builtin/todotools/todo-ask.js +1 -1
  103. package/dist/core/extensions/builtin/todotools/tools/todo.js +4 -1
  104. package/dist/core/resource-loader.js +7 -3
  105. package/dist/modes/interactive/components/ask-user-async-widget.d.ts +2 -0
  106. package/dist/modes/interactive/components/ask-user-async-widget.js +10 -0
  107. package/dist/modes/interactive/components/ask-user-question-state.js +3 -1
  108. package/dist/modes/interactive/interactive-mode.js +7 -1
  109. package/dist/modes/interactive/session-control-commands.d.ts +2 -1
  110. package/dist/modes/interactive/session-control-commands.js +15 -6
  111. package/dist/modes/rpc/connection-question-bridge.js +5 -5
  112. package/dist/modes/rpc/custom-capability.d.ts +6 -0
  113. package/dist/modes/rpc/custom-capability.js +6 -0
  114. package/dist/modes/rpc/extension-ui-response.d.ts +11 -0
  115. package/dist/modes/rpc/extension-ui-response.js +18 -0
  116. package/dist/modes/rpc/host-lifecycle.d.ts +5 -2
  117. package/dist/modes/rpc/host-lifecycle.js +14 -8
  118. package/dist/modes/rpc/rpc-client.d.ts +1 -1
  119. package/dist/modes/rpc/rpc-input-validation.d.ts +1 -1
  120. package/dist/modes/rpc/rpc-input-validation.js +3 -3
  121. package/dist/modes/rpc/rpc-mode.d.ts +3 -2
  122. package/dist/modes/rpc/rpc-mode.js +3 -2
  123. package/dist/modes/rpc/rpc-types.d.ts +4 -3
  124. package/dist/modes/rpc/session-command-router.js +4 -1
  125. package/dist/runtime-snapshot/enter.d.ts +1 -1
  126. package/dist/runtime-snapshot/enter.js +17 -7
  127. package/dist/runtime-snapshot/file-copier.d.ts +24 -0
  128. package/dist/runtime-snapshot/file-copier.js +76 -0
  129. package/dist/runtime-snapshot/layout.d.ts +8 -4
  130. package/dist/runtime-snapshot/layout.js +137 -70
  131. package/dist/runtime-snapshot/registry.d.ts +7 -0
  132. package/dist/runtime-snapshot/registry.js +63 -1
  133. package/dist/senpi +1 -1
  134. package/docs/providers.md +1 -1
  135. package/docs/rpc.md +34 -11
  136. package/package.json +6 -6
  137. package/dist/bundle/chunks/chunk-AWEAVBCR.js +0 -2
  138. package/dist/bundle/chunks/chunk-YLQTKWEC.js +0 -6
  139. package/dist/bundle/chunks/cli-main-ZPMYM3A3.js +0 -3
  140. package/dist/bundle/chunks/session-control-endpoint-UJJGG4WI.js +0 -3
@@ -1,3 +1,4 @@
1
+ import type { QuestionRequest, QuestionResponse } from "../../core/extensions/types.ts";
1
2
  import type { RpcExtensionUIResponse } from "./rpc-types.ts";
2
3
  /**
3
4
  * An `extension_ui_response` carries two ids: `id`, the frame's own correlation id that its reply
@@ -8,6 +9,16 @@ export declare function answeredUiRequestId(frame: {
8
9
  readonly id?: unknown;
9
10
  readonly uiRequestId?: unknown;
10
11
  }): string | undefined;
12
+ /**
13
+ * How a `question` answer settles, identically on a host and a terminal control endpoint: it reads
14
+ * `answers` and `comment` (a `value` or `confirmed` beside them belongs to the other dialogs and is
15
+ * ignored). A non-blank comment settles it `comment-submitted`, the status that carries the comment
16
+ * to the model; otherwise any answer settles it `answered`; neither leaves it unsettled
17
+ * (`question_incomplete`).
18
+ */
19
+ export declare function settledQuestionStatus(answers: QuestionResponse["answers"], comment: string | undefined): "answered" | "comment-submitted" | undefined;
20
+ /** The questions an answer leaves without a selection or text, reported back to the model. */
21
+ export declare function unansweredQuestionIds(questions: QuestionRequest["questions"], answers: QuestionResponse["answers"]): string[];
11
22
  export type ExtensionUiResponseReply = {
12
23
  readonly id: string;
13
24
  readonly type: "response";
@@ -8,6 +8,24 @@ export function answeredUiRequestId(frame) {
8
8
  return frame.uiRequestId;
9
9
  return typeof frame.id === "string" ? frame.id : undefined;
10
10
  }
11
+ /**
12
+ * How a `question` answer settles, identically on a host and a terminal control endpoint: it reads
13
+ * `answers` and `comment` (a `value` or `confirmed` beside them belongs to the other dialogs and is
14
+ * ignored). A non-blank comment settles it `comment-submitted`, the status that carries the comment
15
+ * to the model; otherwise any answer settles it `answered`; neither leaves it unsettled
16
+ * (`question_incomplete`).
17
+ */
18
+ export function settledQuestionStatus(answers, comment) {
19
+ if (comment?.trim())
20
+ return "comment-submitted";
21
+ return Object.keys(answers).length > 0 ? "answered" : undefined;
22
+ }
23
+ /** The questions an answer leaves without a selection or text, reported back to the model. */
24
+ export function unansweredQuestionIds(questions, answers) {
25
+ return questions
26
+ .filter((question) => !answers[question.id]?.selected.length && !answers[question.id]?.text?.trim())
27
+ .map((question) => question.id);
28
+ }
11
29
  /**
12
30
  * Settles one response on a host connection and returns its reply, keyed by the frame `id`.
13
31
  * `undefined` only on an unrouted (single-session stdio) connection for a response no request here
@@ -56,8 +56,11 @@ export declare const INTERNAL_SUPERVISOR_FLAG = "--internal-rpc-host-supervisor"
56
56
  export declare function findInternalSupervisorArgs(argv: readonly string[]): readonly string[] | undefined;
57
57
  /** `--socket <path>` selects the public socket; every other argument is forwarded to the host CLI. */
58
58
  export declare function parseSupervisorArgs(argv: readonly string[]): SupervisorLaunch | undefined;
59
- /** Resolves the committed CLI entry this supervisor wraps (source tree or built dist). */
60
- export declare function resolveCliMainPath(): string;
59
+ /**
60
+ * Resolves the committed CLI entry this supervisor wraps (source tree or built dist).
61
+ * Exported for tests, which pass the module path and layout of a bundled install.
62
+ */
63
+ export declare function resolveCliMainPath(modulePath?: string, bundled?: boolean): string;
61
64
  /**
62
65
  * Resolves the host child spawn. Explicit child commands (desktop launchers)
63
66
  * are forwarded untouched. The default re-enters the committed CLI entry
@@ -193,18 +193,24 @@ function parseSocketIdentity(value) {
193
193
  const match = /^(\d+):(\d+)$/.exec(value);
194
194
  return match ? { dev: Number(match[1]), ino: Number(match[2]) } : undefined;
195
195
  }
196
- /** Resolves the committed CLI entry this supervisor wraps (source tree or built dist). */
197
- export function resolveCliMainPath() {
198
- const modulePath = fileURLToPath(import.meta.url);
196
+ /**
197
+ * Resolves the committed CLI entry this supervisor wraps (source tree or built dist).
198
+ * Exported for tests, which pass the module path and layout of a bundled install.
199
+ */
200
+ export function resolveCliMainPath(modulePath = fileURLToPath(import.meta.url), bundled = isBundledNode) {
201
+ // Bundled, take the entry from the package's own declared bin: the bundle's cli.js beside
202
+ // this chunk, so a host started from a runtime snapshot runs the snapshot's copy and claims
203
+ // it the way a session does (#2409). Counting ".." instead lands on dist/cli-main.js, the
204
+ // unbundled tree the package also ships, which a snapshot links back to the install that an
205
+ // upgrade replaces, or on the package root, where no cli-main was ever emitted.
206
+ const declared = bundled ? resolveDeclaredCliEntry(modulePath) : undefined;
207
+ if (declared !== undefined)
208
+ return declared;
199
209
  const extension = modulePath.endsWith(".ts") ? ".ts" : ".js";
200
210
  const unbundled = resolve(dirname(modulePath), "..", "..", `cli-main${extension}`);
201
211
  if (existsSync(unbundled))
202
212
  return unbundled;
203
- // Bundled, ".." twice reaches the PACKAGE ROOT rather than dist/, naming a cli-main that
204
- // was never emitted. Take the entry from the package's own declared bin instead of
205
- // counting directories: it is the one statement of where the CLI lives that holds in
206
- // every layout. Falls back to the old path when nothing is declared, so a caller that
207
- // was working keeps working.
213
+ // Falls back to the old path when nothing is declared, so a caller that was working keeps working.
208
214
  return resolveDeclaredCliEntry(modulePath) ?? unbundled;
209
215
  }
210
216
  /** The CLI entry declared by the nearest enclosing package.json, when it exists on disk. */
@@ -116,7 +116,7 @@ export declare class RpcClient {
116
116
  retain_on_disconnect?: boolean;
117
117
  /** Per-session auto-titling; needs the host's `auto_title_per_session`. */
118
118
  auto_title?: boolean;
119
- /** Where this session's replies render; needs the host's `prompt_surface`. */
119
+ /** Where this session's replies render; needs the host's `prompt_surface` (`prompt_surface_chat` for `chat`). */
120
120
  promptSurface?: PromptSurface;
121
121
  }): Promise<{
122
122
  sessionId: string;
@@ -31,7 +31,7 @@ export declare function sessionAutoTitleError(value: unknown): string | undefine
31
31
  /**
32
32
  * Detail for an `open_session.promptSurface` the host refuses, or undefined when the value is
33
33
  * absent or a known surface. An unknown value is never read as `terminal`: a client that asked
34
- * for the app prompt must not silently get the routing line.
34
+ * for the app or chat prompt must not silently get the routing line.
35
35
  */
36
36
  export declare function sessionPromptSurfaceError(value: unknown): string | undefined;
37
37
  export declare function rpcCommandShapeError(command: unknown): string | undefined;
@@ -133,12 +133,12 @@ export function sessionAutoTitleError(value) {
133
133
  /**
134
134
  * Detail for an `open_session.promptSurface` the host refuses, or undefined when the value is
135
135
  * absent or a known surface. An unknown value is never read as `terminal`: a client that asked
136
- * for the app prompt must not silently get the routing line.
136
+ * for the app or chat prompt must not silently get the routing line.
137
137
  */
138
138
  export function sessionPromptSurfaceError(value) {
139
- if (value === undefined || value === "terminal" || value === "app")
139
+ if (value === undefined || value === "terminal" || value === "app" || value === "chat")
140
140
  return undefined;
141
- return `promptSurface must be "terminal" or "app".`;
141
+ return `promptSurface must be "terminal", "app" or "chat".`;
142
142
  }
143
143
  export function rpcCommandShapeError(command) {
144
144
  if (typeof command !== "object" || command === null || Array.isArray(command)) {
@@ -49,7 +49,8 @@
49
49
  * applies to CREATE only - re-opening an existing `sessionPath` keeps that file's header
50
50
  * id, so identity is never rewritten by a resume.
51
51
  *
52
- * `open_session.promptSurface` (`terminal` | `app`, host capability `prompt_surface`) builds that
52
+ * `open_session.promptSurface` (`terminal` | `app` | `chat`, host capabilities `prompt_surface` and
53
+ * `prompt_surface_chat`) builds that
53
54
  * session's system prompt for where its replies render; omitted, the host's `SENPI_PROMPT_SURFACE`
54
55
  * decides. An attach naming another surface rebuilds the live session's prompt.
55
56
  *
@@ -60,7 +61,7 @@
60
61
  * `open_failed: <detail>`, `invalid_session_context: <detail>` (a `context` past a
61
62
  * documented cap), `invalid_session_kind: <detail>` (a `kind` that is neither
62
63
  * `interactive` nor `worker`), `invalid_launch_profile: <detail>` (a non-boolean
63
- * `auto_title`, or a `promptSurface` other than `terminal`/`app`), `invalid_session_id: <detail>` (a `durableSessionId` that is not a
64
+ * `auto_title`, or a `promptSurface` other than `terminal`/`app`/`chat`), `invalid_session_id: <detail>` (a `durableSessionId` that is not a
64
65
  * legal session id), `session_id_in_use` (a `durableSessionId` a LIVE session already
65
66
  * holds).
66
67
  *
@@ -49,7 +49,8 @@
49
49
  * applies to CREATE only - re-opening an existing `sessionPath` keeps that file's header
50
50
  * id, so identity is never rewritten by a resume.
51
51
  *
52
- * `open_session.promptSurface` (`terminal` | `app`, host capability `prompt_surface`) builds that
52
+ * `open_session.promptSurface` (`terminal` | `app` | `chat`, host capabilities `prompt_surface` and
53
+ * `prompt_surface_chat`) builds that
53
54
  * session's system prompt for where its replies render; omitted, the host's `SENPI_PROMPT_SURFACE`
54
55
  * decides. An attach naming another surface rebuilds the live session's prompt.
55
56
  *
@@ -60,7 +61,7 @@
60
61
  * `open_failed: <detail>`, `invalid_session_context: <detail>` (a `context` past a
61
62
  * documented cap), `invalid_session_kind: <detail>` (a `kind` that is neither
62
63
  * `interactive` nor `worker`), `invalid_launch_profile: <detail>` (a non-boolean
63
- * `auto_title`, or a `promptSurface` other than `terminal`/`app`), `invalid_session_id: <detail>` (a `durableSessionId` that is not a
64
+ * `auto_title`, or a `promptSurface` other than `terminal`/`app`/`chat`), `invalid_session_id: <detail>` (a `durableSessionId` that is not a
64
65
  * legal session id), `session_id_in_use` (a `durableSessionId` a LIVE session already
65
66
  * holds).
66
67
  *
@@ -478,9 +478,10 @@ export type RpcCommand = (RpcSessionCommand & {
478
478
  /**
479
479
  * Where THIS session's replies render (default: the host's `SENPI_PROMPT_SURFACE`). `app` builds
480
480
  * a prompt with no visible routing line that keeps tool and hook feedback with the agent;
481
- * `terminal` is the classic prompt. A later open that attaches with another value rebuilds
482
- * the session's prompt; an attach without it keeps the current surface. Requires the host
483
- * capability `prompt_surface`. Any other value is refused with `invalid_launch_profile`.
481
+ * `terminal` is the classic prompt; `chat` (a chat bridge) is `app` with no handoff block, ledger
482
+ * lines, or todo handoff cues, and needs `prompt_surface_chat`. A later open that attaches with
483
+ * another value rebuilds the session's prompt; an attach without it keeps the current surface.
484
+ * Requires the host capability `prompt_surface`. Any other value is refused with `invalid_launch_profile`.
484
485
  */
485
486
  promptSurface?: PromptSurface;
486
487
  } | {
@@ -1,6 +1,7 @@
1
1
  import { VERSION } from "../../config.js";
2
+ import { ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY } from "../../core/extensions/builtin/permission-system/config.js";
2
3
  import { buildRpcSessionState } from "./connection-handler.js";
3
- import { AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, PROMPT_SURFACE_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_KIND_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
4
+ import { AUTO_TITLE_PER_SESSION_CAPABILITY, AUTO_TITLE_SESSIONS_CAPABILITY, DURABLE_SESSION_ID_CAPABILITY, MEDIA_PLACEHOLDERS_CAPABILITY, PROMPT_SURFACE_CAPABILITY, PROMPT_SURFACE_CHAT_CAPABILITY, RETAIN_ON_DISCONNECT_CAPABILITY, SESSION_CONTEXT_CAPABILITY, SESSION_KIND_CAPABILITY, WARM_CAPABILITY, } from "./custom-capability.js";
4
5
  import { answerWarm } from "./host-warm.js";
5
6
  import { protocolIdentity } from "./protocol-identity.js";
6
7
  import { sessionAutoTitleError, sessionContextError, sessionKindError, sessionPromptSurfaceError, } from "./rpc-input-validation.js";
@@ -182,6 +183,8 @@ export class SessionCommandRouter {
182
183
  DURABLE_SESSION_ID_CAPABILITY,
183
184
  // Every session's prompt is built from its own launch profile, so one host serves both surfaces.
184
185
  PROMPT_SURFACE_CAPABILITY,
186
+ PROMPT_SURFACE_CHAT_CAPABILITY,
187
+ ACCEPT_EDITS_PERMISSION_PRESET_CAPABILITY,
185
188
  // Only an in-process runtime shares the loop a warm loads into (senpi#2314).
186
189
  ...(this.registry.warm ? [WARM_CAPABILITY] : []),
187
190
  ...(this.connectionOptions?.capabilities ?? []),
@@ -13,5 +13,5 @@ export type RuntimeSnapshotDecision = {
13
13
  * snapshot taken once per build and install, and claims it so it outlives later upgrades.
14
14
  * A launch already running from a snapshot only claims it. Any failure keeps the old behavior.
15
15
  */
16
- export declare function prepareRuntimeSnapshot(entryPath: string, packageDir: string, agentDir: string): RuntimeSnapshotDecision;
16
+ export declare function prepareRuntimeSnapshot(entryPath: string, packageDir: string, agentDir: string): Promise<RuntimeSnapshotDecision>;
17
17
  //# sourceMappingURL=enter.d.ts.map
@@ -4,7 +4,7 @@ import { basename, dirname, join, relative } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { materializeRuntimeSnapshot } from "./layout.js";
6
6
  import { readRuntimeSnapshotMarker } from "./marker.js";
7
- import { claimRuntimeSnapshot, pruneRuntimeSnapshots, withRuntimeLock } from "./registry.js";
7
+ import { claimRuntimeSnapshot, pruneRuntimeSnapshots, withBuildLock, withRuntimeLock } from "./registry.js";
8
8
  export const RUNTIME_MANIFEST = "runtime-manifest.json";
9
9
  function readRuntimeManifest(bundleDir) {
10
10
  let parsed;
@@ -35,7 +35,7 @@ function readRuntimeManifest(bundleDir) {
35
35
  * snapshot taken once per build and install, and claims it so it outlives later upgrades.
36
36
  * A launch already running from a snapshot only claims it. Any failure keeps the old behavior.
37
37
  */
38
- export function prepareRuntimeSnapshot(entryPath, packageDir, agentDir) {
38
+ export async function prepareRuntimeSnapshot(entryPath, packageDir, agentDir) {
39
39
  const bundleDir = dirname(entryPath);
40
40
  const runtimeRoot = join(agentDir, "runtime");
41
41
  try {
@@ -49,15 +49,25 @@ export function prepareRuntimeSnapshot(entryPath, packageDir, agentDir) {
49
49
  const installId = createHash("sha256").update(realpathSync(packageDir)).digest("hex").slice(0, 12);
50
50
  const snapshotId = `${manifest.buildId}-${installId}`;
51
51
  const snapshotDir = join(runtimeRoot, snapshotId);
52
- const ready = withRuntimeLock(runtimeRoot, () => {
53
- if (readRuntimeSnapshotMarker(snapshotDir)?.buildId !== manifest.buildId) {
54
- materializeRuntimeSnapshot(packageDir, snapshotDir, manifest);
55
- }
52
+ const isBuilt = () => readRuntimeSnapshotMarker(snapshotDir)?.buildId === manifest.buildId;
53
+ const claim = () => withRuntimeLock(runtimeRoot, () => {
54
+ if (!isBuilt())
55
+ return false;
56
56
  claimRuntimeSnapshot(snapshotDir, process.pid);
57
57
  pruneRuntimeSnapshots(runtimeRoot, snapshotId, Date.now());
58
58
  return true;
59
59
  });
60
- if (!ready)
60
+ let claimed = claim();
61
+ if (claimed === false) {
62
+ const built = await withBuildLock(runtimeRoot, snapshotId, async () => {
63
+ if (!isBuilt())
64
+ await materializeRuntimeSnapshot(packageDir, snapshotDir, manifest);
65
+ return true;
66
+ });
67
+ if (built)
68
+ claimed = claim();
69
+ }
70
+ if (claimed !== true)
61
71
  return { kind: "run-here" };
62
72
  const entryUrl = pathToFileURL(join(snapshotDir, "dist", "bundle", basename(entryPath))).href;
63
73
  return { kind: "hand-off", entryUrl, snapshotDir };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * How a file gets into a snapshot. A clone (APFS, btrfs, XFS, ReFS) is copy-on-write: free, and
3
+ * independent of the install even if something rewrites the install's file in place. A hardlink
4
+ * is just as free but shares the file itself, so it is only taken where the filesystem cannot
5
+ * clone (ext4, NTFS); package managers replace files rather than rewriting them, which keeps a
6
+ * linked snapshot on the build it was taken from. A plain copy is the last resort (another
7
+ * volume, a filesystem that refuses links).
8
+ */
9
+ export type SnapshotCopyMode = "clone" | "clone-or-copy" | "link" | "copy";
10
+ export interface SnapshotFileOperations {
11
+ copyFile(source: string, target: string, mode: number): Promise<void>;
12
+ link(source: string, target: string): Promise<void>;
13
+ }
14
+ export interface SnapshotFileCopier {
15
+ copy(source: string, target: string): Promise<void>;
16
+ readonly mode: SnapshotCopyMode;
17
+ }
18
+ /**
19
+ * A file copier that starts with the cheapest independent copy and settles on the first mode the
20
+ * filesystem accepts, so a snapshot of thousands of files pays for a refused mode once.
21
+ */
22
+ export declare function createSnapshotFileCopier(platform?: NodeJS.Platform, operations?: SnapshotFileOperations): SnapshotFileCopier;
23
+ export declare function copyFiles(pairs: readonly (readonly [string, string])[], copier: SnapshotFileCopier): Promise<void>;
24
+ //# sourceMappingURL=file-copier.d.ts.map
@@ -0,0 +1,76 @@
1
+ import { constants } from "node:fs";
2
+ import { copyFile, link } from "node:fs/promises";
3
+ function errorCode(error) {
4
+ return error instanceof Error && "code" in error ? error.code : undefined;
5
+ }
6
+ const FILE_OPERATIONS = { copyFile, link };
7
+ /** Clones go through the filesystem's metadata path, which stops scaling past a few in flight. */
8
+ const COPY_CONCURRENCY = 8;
9
+ function nextCopyMode(mode, error, platform) {
10
+ const code = errorCode(error);
11
+ // A missing source or an occupied target is about this file, not about what the filesystem can do.
12
+ if (code === "ENOENT" || code === "ENOTDIR" || code === "EEXIST")
13
+ return undefined;
14
+ switch (mode) {
15
+ case "clone":
16
+ // Node on macOS cannot force a clone (ENOSYS), but its plain clone request clones on APFS
17
+ // and copies elsewhere, which is exactly the order wanted without ever sharing an inode.
18
+ return platform === "darwin" && code === "ENOSYS" ? "clone-or-copy" : "link";
19
+ case "link":
20
+ return "copy";
21
+ case "clone-or-copy":
22
+ case "copy":
23
+ return undefined;
24
+ }
25
+ }
26
+ /**
27
+ * A file copier that starts with the cheapest independent copy and settles on the first mode the
28
+ * filesystem accepts, so a snapshot of thousands of files pays for a refused mode once.
29
+ */
30
+ export function createSnapshotFileCopier(platform = process.platform, operations = FILE_OPERATIONS) {
31
+ let mode = "clone";
32
+ const attempt = (current, source, target) => {
33
+ switch (current) {
34
+ case "clone":
35
+ return operations.copyFile(source, target, constants.COPYFILE_FICLONE_FORCE);
36
+ case "clone-or-copy":
37
+ return operations.copyFile(source, target, constants.COPYFILE_FICLONE);
38
+ case "link":
39
+ return operations.link(source, target);
40
+ case "copy":
41
+ return operations.copyFile(source, target, 0);
42
+ }
43
+ };
44
+ return {
45
+ get mode() {
46
+ return mode;
47
+ },
48
+ async copy(source, target) {
49
+ for (let current = mode;;) {
50
+ try {
51
+ await attempt(current, source, target);
52
+ return;
53
+ }
54
+ catch (error) {
55
+ const next = nextCopyMode(current, error, platform);
56
+ if (next === undefined)
57
+ throw error;
58
+ current = next;
59
+ mode = next;
60
+ }
61
+ }
62
+ },
63
+ };
64
+ }
65
+ export async function copyFiles(pairs, copier) {
66
+ let next = 0;
67
+ const worker = async () => {
68
+ for (let index = next++; index < pairs.length; index = next++) {
69
+ const [source, target] = pairs[index] ?? [];
70
+ if (source !== undefined && target !== undefined)
71
+ await copier.copy(source, target);
72
+ }
73
+ };
74
+ await Promise.all(Array.from({ length: Math.min(COPY_CONCURRENCY, pairs.length) }, worker));
75
+ }
76
+ //# sourceMappingURL=file-copier.js.map
@@ -1,3 +1,4 @@
1
+ import { type SnapshotFileCopier } from "./file-copier.ts";
1
2
  export interface RuntimeManifest {
2
3
  readonly buildId: string;
3
4
  readonly externals: readonly string[];
@@ -6,10 +7,13 @@ export declare class RuntimeSnapshotLayoutError extends Error {
6
7
  readonly packageName: string;
7
8
  constructor(packageName: string);
8
9
  }
10
+ export declare const STAGING_PREFIX = ".tmp-";
9
11
  /**
10
- * Builds `target` as a copy of the package that no reinstall can touch: `dist/bundle` and
11
- * `package.json` are copied, every other entry is linked back to the install. Built beside the
12
- * target and renamed into place, so a crash never leaves a half-built snapshot under its name.
12
+ * Builds `target` as a copy of the package that no reinstall can touch: the package itself and
13
+ * every package its dependency graph reaches, with nothing linked back to the install, so an
14
+ * upgrade that deletes, rewrites or re-lays-out the install (bundledDependencies on or off) never
15
+ * changes what a running session loads (#2408). Built beside the target and renamed into place,
16
+ * so a crash never leaves a half-built snapshot under its name.
13
17
  */
14
- export declare function materializeRuntimeSnapshot(packageDir: string, target: string, manifest: RuntimeManifest): void;
18
+ export declare function materializeRuntimeSnapshot(packageDir: string, target: string, manifest: RuntimeManifest, copier?: SnapshotFileCopier): Promise<void>;
15
19
  //# sourceMappingURL=layout.d.ts.map
@@ -1,5 +1,6 @@
1
- import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
2
  import { basename, dirname, join } from "node:path";
3
+ import { copyFiles, createSnapshotFileCopier } from "./file-copier.js";
3
4
  import { RUNTIME_SNAPSHOT_MARKER } from "./marker.js";
4
5
  export class RuntimeSnapshotLayoutError extends Error {
5
6
  constructor(packageName) {
@@ -7,44 +8,53 @@ export class RuntimeSnapshotLayoutError extends Error {
7
8
  this.packageName = packageName;
8
9
  }
9
10
  }
11
+ export const STAGING_PREFIX = ".tmp-";
12
+ /** Type declarations and source maps: no runtime ever loads them, and they are half the files. */
13
+ const UNLOADED_FILE = /\.(?:d\.[cm]?ts|map)$/;
10
14
  function isMissing(error) {
11
15
  return error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
12
16
  }
13
- function occupied(path) {
14
- try {
15
- lstatSync(path);
16
- return true;
17
- }
18
- catch (error) {
19
- if (isMissing(error))
20
- return false;
21
- throw error;
22
- }
23
- }
24
- function linkEntry(source, target) {
25
- let isDirectory;
26
- try {
27
- isDirectory = statSync(source).isDirectory();
28
- }
29
- catch (error) {
30
- // A dangling link in the install stays unresolvable from the snapshot too.
31
- if (isMissing(error))
32
- return;
33
- throw error;
34
- }
35
- // Windows file symlinks need a privilege; the files involved are small, so copy them.
36
- if (process.platform === "win32" && !isDirectory) {
37
- copyFileSync(source, target);
17
+ /**
18
+ * Creates the directories of a tree copy and lists the files to copy into them, following
19
+ * symlinks so nothing in the result points back at the source. `skipModules` leaves nested
20
+ * `node_modules` out: dependencies are placed on their own.
21
+ */
22
+ function planTree(source, target, files, skipModules, seen = new Set()) {
23
+ const real = realpathSync(source);
24
+ if (seen.has(real))
38
25
  return;
39
- }
40
- symlinkSync(source, target, isDirectory ? "junction" : "file");
41
- }
42
- function linkChildren(sourceDir, targetDir, skip) {
43
- for (const name of readdirSync(sourceDir)) {
44
- if (skip.has(name) || occupied(join(targetDir, name)))
26
+ seen.add(real);
27
+ mkdirSync(target, { recursive: true });
28
+ for (const entry of readdirSync(real, { withFileTypes: true })) {
29
+ if (skipModules && entry.name === "node_modules")
45
30
  continue;
46
- linkEntry(join(sourceDir, name), join(targetDir, name));
31
+ const from = join(real, entry.name);
32
+ const to = join(target, entry.name);
33
+ if (entry.isDirectory()) {
34
+ planTree(from, to, files, skipModules, seen);
35
+ }
36
+ else if (entry.isFile()) {
37
+ if (!UNLOADED_FILE.test(entry.name))
38
+ files.push([from, to]);
39
+ }
40
+ else if (entry.isSymbolicLink()) {
41
+ let isDirectory;
42
+ try {
43
+ isDirectory = statSync(from).isDirectory();
44
+ }
45
+ catch (error) {
46
+ // A dangling link in the install stays unresolvable in the snapshot too.
47
+ if (isMissing(error))
48
+ continue;
49
+ throw error;
50
+ }
51
+ if (isDirectory)
52
+ planTree(from, to, files, skipModules, seen);
53
+ else if (!UNLOADED_FILE.test(entry.name))
54
+ files.push([realpathSync(from), to]);
55
+ }
47
56
  }
57
+ seen.delete(real);
48
58
  }
49
59
  /** The node_modules directories Node's resolver walks from `dir`, nearest first. */
50
60
  function moduleDirectoriesFrom(dir) {
@@ -59,27 +69,6 @@ function moduleDirectoriesFrom(dir) {
59
69
  return directories;
60
70
  }
61
71
  }
62
- /**
63
- * One node_modules that resolves every package name the way the install does: the union of the
64
- * install's resolution path, nearest directory first, so hoisted and nested layouts both hold.
65
- */
66
- function linkModules(packageDir, target) {
67
- mkdirSync(target, { recursive: true });
68
- for (const source of moduleDirectoriesFrom(packageDir)) {
69
- for (const name of readdirSync(source)) {
70
- if (name === ".bin")
71
- continue;
72
- const scoped = name.startsWith("@") && statSync(join(source, name)).isDirectory();
73
- if (!scoped) {
74
- if (!occupied(join(target, name)))
75
- linkEntry(join(source, name), join(target, name));
76
- continue;
77
- }
78
- mkdirSync(join(target, name), { recursive: true });
79
- linkChildren(join(source, name), join(target, name), new Set());
80
- }
81
- }
82
- }
83
72
  function packageRootFrom(dir, packageName) {
84
73
  for (const modules of moduleDirectoriesFrom(dir)) {
85
74
  const root = join(modules, packageName);
@@ -88,31 +77,109 @@ function packageRootFrom(dir, packageName) {
88
77
  }
89
78
  return undefined;
90
79
  }
91
- function verifyExternals(packageDir, snapshotBundleDir, externals) {
80
+ function dependencyNames(packageDir) {
81
+ const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
82
+ if (typeof manifest !== "object" || manifest === null)
83
+ return [];
84
+ const names = new Set();
85
+ for (const field of ["dependencies", "optionalDependencies", "peerDependencies"]) {
86
+ const block = manifest[field];
87
+ if (typeof block === "object" && block !== null)
88
+ for (const name of Object.keys(block))
89
+ names.add(name);
90
+ }
91
+ return [...names];
92
+ }
93
+ function closureNames(packageDir, extra) {
94
+ const names = new Set(extra);
95
+ const visited = new Set();
96
+ const pending = [realpathSync(packageDir)];
97
+ for (let dir = pending.pop(); dir !== undefined; dir = pending.pop()) {
98
+ if (visited.has(dir))
99
+ continue;
100
+ visited.add(dir);
101
+ for (const name of dependencyNames(dir)) {
102
+ names.add(name);
103
+ const root = packageRootFrom(dir, name);
104
+ if (root !== undefined && !visited.has(root))
105
+ pending.push(root);
106
+ }
107
+ }
108
+ return names;
109
+ }
110
+ /**
111
+ * The snapshot's own `node_modules`: a copy of every package the install's dependency graph
112
+ * reaches, placed so each one resolves its dependencies to copies of exactly the packages the
113
+ * install resolves them to. Each name first goes where the package itself would find it (the
114
+ * nearest copy, nested or hoisted); a package that needs another copy of a name gets it nested
115
+ * under itself. Returns the snapshot path of each placed package and the install copy it holds.
116
+ */
117
+ function planModules(packageDir, snapshotRoot, externals, files) {
118
+ const self = realpathSync(packageDir);
119
+ const placed = new Map();
120
+ const pending = [];
121
+ const place = (installDir, snapshotDir) => {
122
+ planTree(installDir, snapshotDir, files, true);
123
+ placed.set(snapshotDir, installDir);
124
+ pending.push({ installDir, snapshotDir });
125
+ };
126
+ const resolvePlaced = (from, name) => {
127
+ for (let dir = from;; dir = dirname(dir)) {
128
+ if (basename(dir) !== "node_modules") {
129
+ const found = placed.get(join(dir, "node_modules", name));
130
+ if (found !== undefined)
131
+ return found;
132
+ }
133
+ if (dir === snapshotRoot || dirname(dir) === dir)
134
+ return undefined;
135
+ }
136
+ };
137
+ for (const name of closureNames(packageDir, externals)) {
138
+ const source = packageRootFrom(packageDir, name);
139
+ if (source !== undefined && source !== self)
140
+ place(source, join(snapshotRoot, "node_modules", name));
141
+ }
142
+ for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
143
+ for (const name of dependencyNames(next.installDir)) {
144
+ const expected = packageRootFrom(next.installDir, name);
145
+ if (expected === undefined || expected === self)
146
+ continue;
147
+ if (resolvePlaced(next.snapshotDir, name) !== expected) {
148
+ place(expected, join(next.snapshotDir, "node_modules", name));
149
+ }
150
+ }
151
+ }
152
+ return placed;
153
+ }
154
+ /** Each external the bundle imports must resolve, from the snapshot's bundle, to the snapshot's copy of the install's package. */
155
+ function verifyExternals(packageDir, snapshotBundleDir, externals, placed) {
92
156
  for (const name of externals) {
93
157
  const expected = packageRootFrom(packageDir, name);
94
- if (expected !== undefined && packageRootFrom(snapshotBundleDir, name) !== expected) {
158
+ if (expected === undefined)
159
+ continue;
160
+ const found = moduleDirectoriesFrom(snapshotBundleDir)
161
+ .map((modules) => join(modules, name))
162
+ .find((root) => existsSync(join(root, "package.json")));
163
+ if (found === undefined || placed.get(found) !== expected)
95
164
  throw new RuntimeSnapshotLayoutError(name);
96
- }
97
165
  }
98
166
  }
99
167
  /**
100
- * Builds `target` as a copy of the package that no reinstall can touch: `dist/bundle` and
101
- * `package.json` are copied, every other entry is linked back to the install. Built beside the
102
- * target and renamed into place, so a crash never leaves a half-built snapshot under its name.
168
+ * Builds `target` as a copy of the package that no reinstall can touch: the package itself and
169
+ * every package its dependency graph reaches, with nothing linked back to the install, so an
170
+ * upgrade that deletes, rewrites or re-lays-out the install (bundledDependencies on or off) never
171
+ * changes what a running session loads (#2408). Built beside the target and renamed into place,
172
+ * so a crash never leaves a half-built snapshot under its name.
103
173
  */
104
- export function materializeRuntimeSnapshot(packageDir, target, manifest) {
105
- const staging = join(dirname(target), `.tmp-${basename(target)}-${process.pid}`);
174
+ export async function materializeRuntimeSnapshot(packageDir, target, manifest, copier = createSnapshotFileCopier()) {
175
+ const staging = join(dirname(target), `${STAGING_PREFIX}${basename(target)}-${process.pid}`);
106
176
  rmSync(staging, { recursive: true, force: true });
107
177
  try {
108
- const snapshotBundleDir = join(staging, "dist", "bundle");
109
- mkdirSync(join(staging, "dist"), { recursive: true });
110
- cpSync(join(packageDir, "dist", "bundle"), snapshotBundleDir, { recursive: true });
111
- copyFileSync(join(packageDir, "package.json"), join(staging, "package.json"));
112
- linkChildren(packageDir, staging, new Set(["dist", "package.json", "node_modules"]));
113
- linkChildren(join(packageDir, "dist"), join(staging, "dist"), new Set(["bundle"]));
114
- linkModules(packageDir, join(staging, "node_modules"));
115
- verifyExternals(packageDir, join(snapshotBundleDir, "chunks"), manifest.externals);
178
+ const files = [];
179
+ planTree(packageDir, staging, files, true);
180
+ const placed = planModules(packageDir, staging, manifest.externals, files);
181
+ await copyFiles(files, copier);
182
+ verifyExternals(packageDir, join(staging, "dist", "bundle", "chunks"), manifest.externals, placed);
116
183
  const marker = { buildId: manifest.buildId, installPackageDir: realpathSync(packageDir) };
117
184
  writeFileSync(join(staging, RUNTIME_SNAPSHOT_MARKER), `${JSON.stringify(marker)}\n`);
118
185
  rmSync(target, { recursive: true, force: true });