@dynamicagents/plugins 0.11.1 → 0.13.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 (134) hide show
  1. package/README.md +59 -79
  2. package/dist/browser/index.d.ts +0 -2
  3. package/dist/browser/index.js +21 -27
  4. package/dist/claude-code/cards.d.ts +53 -0
  5. package/dist/claude-code/cards.js +272 -0
  6. package/dist/claude-code/config.d.ts +73 -31
  7. package/dist/claude-code/config.js +0 -2
  8. package/dist/claude-code/credentials.d.ts +21 -9
  9. package/dist/claude-code/credentials.js +43 -9
  10. package/dist/claude-code/egress.d.ts +15 -14
  11. package/dist/claude-code/egress.js +51 -14
  12. package/dist/claude-code/events.d.ts +153 -9
  13. package/dist/claude-code/events.js +289 -58
  14. package/dist/claude-code/github.d.ts +39 -0
  15. package/dist/claude-code/github.js +82 -0
  16. package/dist/claude-code/index.d.ts +24 -80
  17. package/dist/claude-code/index.js +19 -234
  18. package/dist/claude-code/model.d.ts +152 -0
  19. package/dist/claude-code/model.js +342 -0
  20. package/dist/claude-code/run.d.ts +283 -92
  21. package/dist/claude-code/run.js +620 -114
  22. package/dist/claude-code/session.d.ts +131 -0
  23. package/dist/claude-code/session.js +285 -0
  24. package/dist/claude-code/spec.d.ts +46 -0
  25. package/dist/claude-code/spec.js +83 -0
  26. package/dist/claude-code/verified.d.ts +15 -0
  27. package/dist/claude-code/verified.js +15 -0
  28. package/dist/computer/context.d.ts +19 -0
  29. package/dist/computer/context.js +222 -0
  30. package/dist/computer/file-lock.d.ts +5 -0
  31. package/dist/computer/file-lock.js +40 -0
  32. package/dist/computer/gate.d.ts +12 -12
  33. package/dist/computer/gate.js +12 -12
  34. package/dist/computer/index.d.ts +57 -255
  35. package/dist/computer/index.js +98 -1039
  36. package/dist/computer/paths.d.ts +47 -22
  37. package/dist/computer/paths.js +64 -49
  38. package/dist/computer/proxy.d.ts +45 -0
  39. package/dist/computer/proxy.js +234 -0
  40. package/dist/computer/render.d.ts +27 -38
  41. package/dist/computer/render.js +32 -65
  42. package/dist/computer/tools-exec.d.ts +3 -0
  43. package/dist/computer/tools-exec.js +268 -0
  44. package/dist/computer/tools-file.d.ts +12 -0
  45. package/dist/computer/tools-file.js +64 -0
  46. package/dist/computer/tools-grep.d.ts +3 -0
  47. package/dist/computer/tools-grep.js +129 -0
  48. package/dist/repo/checkout.d.ts +35 -6
  49. package/dist/repo/checkout.js +64 -6
  50. package/dist/repo/context.d.ts +135 -0
  51. package/dist/repo/context.js +582 -0
  52. package/dist/repo/index.d.ts +79 -57
  53. package/dist/repo/index.js +118 -1112
  54. package/dist/repo/tools-checks.d.ts +3 -0
  55. package/dist/repo/tools-checks.js +112 -0
  56. package/dist/repo/tools-clone.d.ts +4 -0
  57. package/dist/repo/tools-clone.js +192 -0
  58. package/dist/repo/tools-pr.d.ts +14 -0
  59. package/dist/repo/tools-pr.js +196 -0
  60. package/dist/repo/tools-review.d.ts +12 -0
  61. package/dist/repo/tools-review.js +393 -0
  62. package/dist/repo/tools-worktree.d.ts +4 -0
  63. package/dist/repo/tools-worktree.js +305 -0
  64. package/dist/repo/tools-worktrees.d.ts +12 -0
  65. package/dist/repo/tools-worktrees.js +48 -0
  66. package/dist/repo/url.d.ts +2 -2
  67. package/dist/repo/url.js +7 -7
  68. package/dist/scratch/index.d.ts +4 -4
  69. package/dist/scratch/index.js +12 -11
  70. package/dist/{computer → workspace}/advisory.d.ts +33 -20
  71. package/dist/{computer → workspace}/advisory.js +32 -7
  72. package/dist/workspace/ca-trust.d.ts +95 -0
  73. package/dist/workspace/ca-trust.js +312 -0
  74. package/dist/workspace/container-deps.d.ts +54 -0
  75. package/dist/workspace/container-deps.js +224 -0
  76. package/dist/workspace/container-fault.d.ts +38 -0
  77. package/dist/workspace/container-fault.js +73 -0
  78. package/dist/workspace/exec.d.ts +94 -0
  79. package/dist/workspace/exec.js +153 -0
  80. package/dist/workspace/format.d.ts +36 -0
  81. package/dist/workspace/format.js +57 -0
  82. package/dist/workspace/git-host.d.ts +79 -0
  83. package/dist/workspace/git-host.js +226 -0
  84. package/dist/workspace/git-identity.d.ts +57 -0
  85. package/dist/workspace/git-identity.js +147 -0
  86. package/dist/workspace/index.d.ts +43 -47
  87. package/dist/workspace/index.js +52 -112
  88. package/dist/workspace/install-job.d.ts +121 -0
  89. package/dist/workspace/install-job.js +866 -0
  90. package/dist/{computer → workspace}/install.d.ts +15 -19
  91. package/dist/{computer → workspace}/install.js +8 -11
  92. package/dist/workspace/object.d.ts +502 -0
  93. package/dist/workspace/object.js +1427 -0
  94. package/dist/workspace/open.d.ts +95 -0
  95. package/dist/workspace/open.js +63 -0
  96. package/dist/workspace/read.d.ts +16 -0
  97. package/dist/workspace/read.js +22 -0
  98. package/dist/workspace/shell.d.ts +45 -0
  99. package/dist/workspace/shell.js +80 -0
  100. package/dist/workspace/sync.d.ts +111 -0
  101. package/dist/workspace/sync.js +124 -0
  102. package/dist/workspace/wake.d.ts +19 -0
  103. package/dist/workspace/wake.js +10 -0
  104. package/package.json +28 -42
  105. package/dist/arc-agi/analysis.d.ts +0 -336
  106. package/dist/arc-agi/analysis.js +0 -829
  107. package/dist/arc-agi/client.d.ts +0 -82
  108. package/dist/arc-agi/client.js +0 -113
  109. package/dist/arc-agi/game-tools.d.ts +0 -25
  110. package/dist/arc-agi/game-tools.js +0 -26
  111. package/dist/arc-agi/index.d.ts +0 -42
  112. package/dist/arc-agi/index.js +0 -175
  113. package/dist/arc-agi/main-agent.d.ts +0 -27
  114. package/dist/arc-agi/main-agent.js +0 -65
  115. package/dist/arc-agi/recipe.d.ts +0 -51
  116. package/dist/arc-agi/recipe.js +0 -100
  117. package/dist/arc-agi/scorecard.d.ts +0 -118
  118. package/dist/arc-agi/scorecard.js +0 -163
  119. package/dist/arc-agi/soul.d.ts +0 -40
  120. package/dist/arc-agi/soul.js +0 -90
  121. package/dist/arc-agi/store.d.ts +0 -154
  122. package/dist/arc-agi/store.js +0 -216
  123. package/dist/arc-agi/tools.d.ts +0 -36
  124. package/dist/arc-agi/tools.js +0 -576
  125. package/dist/arc-agi/types.d.ts +0 -212
  126. package/dist/arc-agi/types.js +0 -19
  127. package/dist/claude-code/recipe.d.ts +0 -53
  128. package/dist/claude-code/recipe.js +0 -100
  129. package/dist/computer/read.d.ts +0 -137
  130. package/dist/computer/read.js +0 -203
  131. package/dist/recall/index.d.ts +0 -99
  132. package/dist/recall/index.js +0 -171
  133. package/dist/triage/index.d.ts +0 -355
  134. package/dist/triage/index.js +0 -302
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * Which paths these tools act on, and the sentence a refused one gets.
3
3
  *
4
- * Two lists that look alike and mean opposite things. `node_modules` is *absent*
5
- * — physics, decided by `computerd` — so its note explains a file that is really
6
- * there and names the tool that can reach it. `.git` is present, readable, and
7
- * refused anyway — policy, decided here — so its note names where repository work
8
- * belongs. Two reasons want two sentences, which is why they are not one list.
4
+ * Each refused directory is also skipped by every walk, though the walk steps
5
+ * over more than it refuses — see {@link WALK_SKIPS}. `.git` is refused as
6
+ * policy: a model that edits `.git/HEAD` corrupts a checkout in a way that
7
+ * surfaces much later. `node_modules` is refused as a fact: it lives on the
8
+ * container's disk (see `../workspace/container-deps.ts`), so these tools, which read
9
+ * the workspace, would find it empty.
9
10
  *
10
11
  * Everything here is a string comparison: no filesystem, no `await`, which is
11
12
  * what lets {@link guardPath} run before a tool opens the workspace at all.
12
13
  */
13
- export declare function isContainerOnly(path: string): boolean;
14
14
  /**
15
15
  * Is this path inside git's internal state?
16
16
  *
@@ -18,33 +18,58 @@ export declare function isContainerOnly(path: string): boolean;
18
18
  * checkout in a way that surfaces much later as an inexplicable git failure.
19
19
  *
20
20
  * Exact segment rather than substring, so `.gitignore`, `.gitattributes` and
21
- * `.github/` are untouched — the same trap `node_modules_old` sets one function up.
21
+ * `.github/` are untouched.
22
22
  */
23
23
  export declare function isGitInternal(path: string): boolean;
24
24
  /**
25
- * The path check every file tool makes, before it opens anything.
25
+ * Where a session's own state lives inside a workspace, as a directory name.
26
26
  *
27
- * Returns the sentence to hand back, or `undefined` to proceed. Two string
28
- * comparisons — no `stat`, no round trip — which is why all six tools can afford
29
- * to call it before the workspace is opened.
27
+ * Every Claude Code session keeps its `CLAUDE_CONFIG_DIR` in a directory of
28
+ * this name at the workspace root — the Durable Object's storage, so a
29
+ * conversation outlives its container and a later session can continue it. See
30
+ * {@link file://../claude-code/run.ts SESSION_CONFIG_DIR}.
31
+ *
32
+ * **Spelled here as well as in the plugin that sets the variable**, because the
33
+ * module that has to step over it is the walk below, and neither may import the
34
+ * other: `/computer` reaching into `/claude-code` would pull a whole plugin into
35
+ * every agent that installs the file tools, and `/claude-code` reaches no other
36
+ * subpath — `verify:exports` refuses both. A spec in `/claude-code` holds the
37
+ * two spellings to one.
38
+ */
39
+ export declare const SESSION_STATE_DIR = ".claude-sessions";
40
+ /**
41
+ * What every walk steps over, as directory names. Both the workspace's `glob`
42
+ * and the `grep` tool hand these to the store as `**\/<name>` exclusions, which
43
+ * prunes them: no directory named here is descended into, so none costs a read.
44
+ *
45
+ * {@link SESSION_STATE_DIR} is here for a reason the refused directories do not
46
+ * share: a transcript is a verbatim record of everything a session read, so a
47
+ * walk that descended into it would answer a `grep` for a line of source with
48
+ * the session that quoted it.
49
+ */
50
+ export declare const WALK_SKIPS: readonly [".git", "node_modules", ".claude-sessions"];
51
+ /**
52
+ * The path check every file tool and every workspace call makes, before it
53
+ * opens anything.
54
+ *
55
+ * Returns the sentence to hand back, or `undefined` to proceed. One string
56
+ * comparison — no `stat`, no round trip — which is why every file tool can
57
+ * afford to call it before the workspace is opened.
30
58
  *
31
59
  * ## A redirect, not a boundary
32
60
  *
33
- * `node_modules` is a fact about the substrate and the note names the tool that
34
- * can see it; `.git` is a rule about where repository work belongs and the note
35
- * names the tools it belongs to. Both catch the case that actually happens — a
36
- * model reaching into `.git/HEAD` to fix a merge, or reading a dependency's
37
- * source and finding nothing there.
61
+ * The note names the tools repository work belongs to, because the case that
62
+ * actually happens is a model reaching into `.git/HEAD` to fix a merge.
38
63
  *
39
- * It is not containment, and this is the only place worth saying so. `sb_exec`
40
- * is in the same tool family, granted per family rather than per tool, so every
41
- * agent holding these six also holds a shell that reads and writes `.git`
42
- * directly.
64
+ * It is not containment, and this is the only place worth saying so. An agent
65
+ * that can write files here almost always holds `bash` too, which reads and
66
+ * writes `.git` directly.
43
67
  *
44
68
  * Symlinks are not resolved, for the same reason: a tracked
45
69
  * `docs/notes.md -> ../.git/config` only matters to an agent given these tools
46
- * *without* the shell, which the family granularity makes unbuildable. If that
47
- * ever becomes buildable, the **write** path is the half to restore —
70
+ * *without* the shell. A read-only install — `restrictTools` down to `grep`,
71
+ * with Think's writing tools left out — is that agent, and it cannot write. If a writing agent
72
+ * without the shell ever exists, the **write** path is the half to restore —
48
73
  * `.git/config` is an input to the credentialed push, and a planted hook runs
49
74
  * under a container-side `repo_commit`. Reading `.git` discloses nothing that is
50
75
  * not already readable, so the read half is not worth an `lstat` per call. The
@@ -1,35 +1,19 @@
1
1
  /**
2
2
  * Which paths these tools act on, and the sentence a refused one gets.
3
3
  *
4
- * Two lists that look alike and mean opposite things. `node_modules` is *absent*
5
- * — physics, decided by `computerd` — so its note explains a file that is really
6
- * there and names the tool that can reach it. `.git` is present, readable, and
7
- * refused anyway — policy, decided here — so its note names where repository work
8
- * belongs. Two reasons want two sentences, which is why they are not one list.
4
+ * Each refused directory is also skipped by every walk, though the walk steps
5
+ * over more than it refuses — see {@link WALK_SKIPS}. `.git` is refused as
6
+ * policy: a model that edits `.git/HEAD` corrupts a checkout in a way that
7
+ * surfaces much later. `node_modules` is refused as a fact: it lives on the
8
+ * container's disk (see `../workspace/container-deps.ts`), so these tools, which read
9
+ * the workspace, would find it empty.
9
10
  *
10
11
  * Everything here is a string comparison: no filesystem, no `await`, which is
11
12
  * what lets {@link guardPath} run before a tool opens the workspace at all.
12
13
  */
13
- /**
14
- * Whether a path sits under a directory the workspace never receives.
15
- *
16
- * Only `node_modules` today, and it is not configurable here because it is not
17
- * our choice: `computerd` applies its own `DEFAULT_IGNORE` container-side, and
18
- * this list exists to *predict* that so a read can explain itself rather than
19
- * reporting a missing file. A dependency the model can plainly see in a `ls` but
20
- * cannot read is the kind of thing that sends it hunting for the wrong bug.
21
- */
22
- const CONTAINER_ONLY_SEGMENTS = ["node_modules"];
23
- export function isContainerOnly(path) {
24
- return path
25
- .split("/")
26
- .some((segment) => CONTAINER_ONLY_SEGMENTS.includes(segment));
27
- }
28
- /** The sentence a container-only path gets instead of "not found". */
29
- function containerOnlyNote(path, verb) {
30
- return (`${path} is inside node_modules, which lives only in the container and is ` +
31
- `not part of the durable workspace — ${verb} cannot see it. Use \`sb_exec\` ` +
32
- `(for example \`cat ${path}\`) to read it through the shell instead.`);
14
+ /** Whether any segment of `path` is exactly `segment`. */
15
+ function hasSegment(path, segment) {
16
+ return path.split("/").includes(segment);
33
17
  }
34
18
  /**
35
19
  * Is this path inside git's internal state?
@@ -38,10 +22,43 @@ function containerOnlyNote(path, verb) {
38
22
  * checkout in a way that surfaces much later as an inexplicable git failure.
39
23
  *
40
24
  * Exact segment rather than substring, so `.gitignore`, `.gitattributes` and
41
- * `.github/` are untouched — the same trap `node_modules_old` sets one function up.
25
+ * `.github/` are untouched.
42
26
  */
43
27
  export function isGitInternal(path) {
44
- return path.split("/").includes(".git");
28
+ return hasSegment(path, ".git");
29
+ }
30
+ /**
31
+ * Where a session's own state lives inside a workspace, as a directory name.
32
+ *
33
+ * Every Claude Code session keeps its `CLAUDE_CONFIG_DIR` in a directory of
34
+ * this name at the workspace root — the Durable Object's storage, so a
35
+ * conversation outlives its container and a later session can continue it. See
36
+ * {@link file://../claude-code/run.ts SESSION_CONFIG_DIR}.
37
+ *
38
+ * **Spelled here as well as in the plugin that sets the variable**, because the
39
+ * module that has to step over it is the walk below, and neither may import the
40
+ * other: `/computer` reaching into `/claude-code` would pull a whole plugin into
41
+ * every agent that installs the file tools, and `/claude-code` reaches no other
42
+ * subpath — `verify:exports` refuses both. A spec in `/claude-code` holds the
43
+ * two spellings to one.
44
+ */
45
+ export const SESSION_STATE_DIR = ".claude-sessions";
46
+ /**
47
+ * What every walk steps over, as directory names. Both the workspace's `glob`
48
+ * and the `grep` tool hand these to the store as `**\/<name>` exclusions, which
49
+ * prunes them: no directory named here is descended into, so none costs a read.
50
+ *
51
+ * {@link SESSION_STATE_DIR} is here for a reason the refused directories do not
52
+ * share: a transcript is a verbatim record of everything a session read, so a
53
+ * walk that descended into it would answer a `grep` for a line of source with
54
+ * the session that quoted it.
55
+ */
56
+ export const WALK_SKIPS = [".git", "node_modules", SESSION_STATE_DIR];
57
+ /** The sentence a `node_modules` path gets. */
58
+ function dependencyTreeNote(path, verb) {
59
+ return (`${path} is inside node_modules, which lives on the container's disk rather ` +
60
+ `than in the workspace ${verb} works on. Use bash there — ` +
61
+ `\`cat\`, \`ls\`, \`rg\`.`);
45
62
  }
46
63
  /**
47
64
  * The sentence a `.git` path gets.
@@ -49,9 +66,9 @@ export function isGitInternal(path) {
49
66
  * Names a route deliberately: a refusal with no destination is worse than no
50
67
  * refusal, because the model retries and then works around it.
51
68
  *
52
- * What it must never name is `sb_exec`. That hands back the exact capability
69
+ * What it must never name is `bash`. That hands back the exact capability
53
70
  * being withheld, in the one place the model is already looking for a way around
54
- * it, with the tool's own authority behind it. `sb_exec` is unguarded because a
71
+ * it, with the tool's own authority behind it. `bash` is unguarded because a
55
72
  * shell takes an opaque command string and pattern-matching git out of one is
56
73
  * neither reliable nor this guard's job — a fact about the implementation, not a
57
74
  * route to advertise.
@@ -60,34 +77,32 @@ function gitInternalNote(path, verb) {
60
77
  return (`${path} is inside .git — git's internal state, which ${verb} does not touch. ` +
61
78
  `Reading it tells you less than the repository tools do, and writing it ` +
62
79
  `corrupts the checkout. Repository work goes through the repo tools ` +
63
- `(\`repo_status\`, \`repo_diff\`, \`repo_commit\`, \`repo_push\`), which the ` +
64
- `main agent holds. If this task needs git state you cannot get that way, say ` +
65
- `so in your result rather than reaching into .git yourself.`);
80
+ `(\`repo_status\`, \`repo_diff\`, \`repo_commit\`, \`repo_push\`). If this ` +
81
+ `task needs git state you cannot get that way, say so in your result rather ` +
82
+ `than reaching into .git yourself.`);
66
83
  }
67
84
  /**
68
- * The path check every file tool makes, before it opens anything.
85
+ * The path check every file tool and every workspace call makes, before it
86
+ * opens anything.
69
87
  *
70
- * Returns the sentence to hand back, or `undefined` to proceed. Two string
71
- * comparisons — no `stat`, no round trip — which is why all six tools can afford
72
- * to call it before the workspace is opened.
88
+ * Returns the sentence to hand back, or `undefined` to proceed. One string
89
+ * comparison — no `stat`, no round trip — which is why every file tool can
90
+ * afford to call it before the workspace is opened.
73
91
  *
74
92
  * ## A redirect, not a boundary
75
93
  *
76
- * `node_modules` is a fact about the substrate and the note names the tool that
77
- * can see it; `.git` is a rule about where repository work belongs and the note
78
- * names the tools it belongs to. Both catch the case that actually happens — a
79
- * model reaching into `.git/HEAD` to fix a merge, or reading a dependency's
80
- * source and finding nothing there.
94
+ * The note names the tools repository work belongs to, because the case that
95
+ * actually happens is a model reaching into `.git/HEAD` to fix a merge.
81
96
  *
82
- * It is not containment, and this is the only place worth saying so. `sb_exec`
83
- * is in the same tool family, granted per family rather than per tool, so every
84
- * agent holding these six also holds a shell that reads and writes `.git`
85
- * directly.
97
+ * It is not containment, and this is the only place worth saying so. An agent
98
+ * that can write files here almost always holds `bash` too, which reads and
99
+ * writes `.git` directly.
86
100
  *
87
101
  * Symlinks are not resolved, for the same reason: a tracked
88
102
  * `docs/notes.md -> ../.git/config` only matters to an agent given these tools
89
- * *without* the shell, which the family granularity makes unbuildable. If that
90
- * ever becomes buildable, the **write** path is the half to restore —
103
+ * *without* the shell. A read-only install — `restrictTools` down to `grep`,
104
+ * with Think's writing tools left out — is that agent, and it cannot write. If a writing agent
105
+ * without the shell ever exists, the **write** path is the half to restore —
91
106
  * `.git/config` is an input to the credentialed push, and a planted hook runs
92
107
  * under a container-side `repo_commit`. Reading `.git` discloses nothing that is
93
108
  * not already readable, so the read half is not worth an `lstat` per call. The
@@ -96,9 +111,9 @@ function gitInternalNote(path, verb) {
96
111
  * segment.
97
112
  */
98
113
  export function guardPath(path, verb) {
99
- if (isContainerOnly(path))
100
- return containerOnlyNote(path, verb);
101
114
  if (isGitInternal(path))
102
115
  return gitInternalNote(path, verb);
116
+ if (hasSegment(path, "node_modules"))
117
+ return dependencyTreeNote(path, verb);
103
118
  return undefined;
104
119
  }
@@ -0,0 +1,45 @@
1
+ import type { WorkspaceLike } from "@cloudflare/think";
2
+ import type { WorkspaceHost } from "../workspace/open.js";
3
+ import type { ComputerConfig } from "./index.js";
4
+ /**
5
+ * The container's workspace, as the agent's own `this.workspace`.
6
+ *
7
+ * Think's `read`, `write`, `edit` and `delete` run against whatever an agent's
8
+ * workspace is, and `computer()`'s tools replace only the ones it names. So an
9
+ * agent with a container and Think's default workspace would run `bash` in the
10
+ * container and `write` in its own SQLite, and nothing would say so. An agent
11
+ * that installs `computer` sets this instead:
12
+ *
13
+ * ```ts
14
+ * override workspace = computerWorkspace(config, () => this.pluginContext().runtime());
15
+ * ```
16
+ *
17
+ * and `computer(config).tools(ctx)` refuses to start without it — and reaches
18
+ * the workspace through it, so the tools and Think's file tools cannot resolve
19
+ * two different ones.
20
+ *
21
+ * Built on the workspace's own filesystem, not on `@cloudflare/computer`'s
22
+ * Think adapter: that one's `glob` and `readDir` read every entry, and its
23
+ * `writeFile` knows nothing of the paths and the states this plugin refuses.
24
+ */
25
+ /**
26
+ * Where a {@link computerWorkspace} sends each call, carried on it under
27
+ * {@link ROUTE}. `computer()`'s tools route through this rather than through
28
+ * their own config, which is what makes a workspace built from another config,
29
+ * or without its runtime, a wrong workspace for both rather than a split.
30
+ */
31
+ export interface WorkspaceRoute {
32
+ /** The workspace the running turn names. Read per call. */
33
+ name(): string;
34
+ host(name: string): DurableObjectStub<WorkspaceHost>;
35
+ }
36
+ /** The route of a workspace {@link computerWorkspace} built, or `undefined`. */
37
+ export declare function workspaceRoute(workspace: unknown): WorkspaceRoute | undefined;
38
+ /** Whether `workspace` is one {@link computerWorkspace} built. */
39
+ export declare function isComputerWorkspace(workspace: unknown): boolean;
40
+ /**
41
+ * @param runtime The running sub-agent's `runtime()`, read per call — how a
42
+ * sub-agent's file tools reach the workspace its parent prepared. A parent's
43
+ * is `undefined`, and {@link ComputerConfig.workspaceName} names it.
44
+ */
45
+ export declare function computerWorkspace(config: ComputerConfig, runtime?: () => unknown): WorkspaceLike;
@@ -0,0 +1,234 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { withFileLock } from "./file-lock.js";
54
+ import { writeGate } from "./gate.js";
55
+ import { openWorkspaceFs, workspaceNameFromRuntime } from "../workspace/open.js";
56
+ import { guardPath, WALK_SKIPS } from "./paths.js";
57
+ /**
58
+ * The key the route is carried under. `Symbol.for`, so a second copy of this
59
+ * module in one bundle still recognises the first's workspaces.
60
+ */
61
+ const ROUTE = Symbol.for("@dynamicagents/plugins/computer:workspace");
62
+ /**
63
+ * The most one `glob` returns. Its caller is Think's `find`, which shows 200
64
+ * and says when there were more — so one past that is all it can use, and
65
+ * reading the rest of a large checkout into the object would buy nothing.
66
+ */
67
+ const GLOB_LIMIT = 201;
68
+ /** The route of a workspace {@link computerWorkspace} built, or `undefined`. */
69
+ export function workspaceRoute(workspace) {
70
+ if (typeof workspace !== "object" || workspace === null)
71
+ return undefined;
72
+ return workspace[ROUTE];
73
+ }
74
+ /** Whether `workspace` is one {@link computerWorkspace} built. */
75
+ export function isComputerWorkspace(workspace) {
76
+ return workspaceRoute(workspace) !== undefined;
77
+ }
78
+ /**
79
+ * Missing, as the filesystem says it across the boundary. The code does not
80
+ * always survive RPC, so the message is read too.
81
+ */
82
+ function isMissing(err) {
83
+ if (!err || typeof err !== "object")
84
+ return false;
85
+ const { code, message } = err;
86
+ if (code === "ENOENT")
87
+ return true;
88
+ return typeof message === "string" && /ENOENT|no such/i.test(message);
89
+ }
90
+ /** `null` for a path that is not there, which is what Think's tools expect. */
91
+ async function orNull(read) {
92
+ try {
93
+ return await read;
94
+ }
95
+ catch (err) {
96
+ if (isMissing(err))
97
+ return null;
98
+ throw err;
99
+ }
100
+ }
101
+ /**
102
+ * `stat`, with a missing path answered as `null`.
103
+ *
104
+ * Asked of the stub's own `statOrNull` where it has one, the way
105
+ * {@link file://../workspace/read.ts pathExists} asks for `exists`: Think's
106
+ * `read` stats before it reads, so a missing file is an ordinary answer here,
107
+ * and a `stat` that throws across the boundary leaves the object an unhandled
108
+ * rejection to log for it.
109
+ */
110
+ function statOrNull(fs, path) {
111
+ // Called as a method, never through `.call`: on an RPC stub `.call` is read
112
+ // as a remote method.
113
+ const remote = fs;
114
+ if (typeof remote.statOrNull === "function")
115
+ return remote.statOrNull(path);
116
+ return orNull(fs.stat(path));
117
+ }
118
+ async function drain(stream) {
119
+ return new Uint8Array(await new Response(stream).arrayBuffer());
120
+ }
121
+ function fileInfo(entry) {
122
+ const type = entry.isDirectory ? "directory" : "file";
123
+ return {
124
+ path: entry.path,
125
+ name: entry.path.slice(entry.path.lastIndexOf("/") + 1),
126
+ type,
127
+ // Generic on purpose: Think's `read` sniffs a generic type from the bytes,
128
+ // and an extension is not evidence of what a file holds.
129
+ mimeType: type === "directory" ? "inode/directory" : "application/octet-stream",
130
+ size: entry.size ?? 0,
131
+ createdAt: entry.mtime ?? 0,
132
+ updatedAt: entry.mtime ?? 0
133
+ };
134
+ }
135
+ /** The directory a glob walks from, and the pattern relative to it. */
136
+ function splitGlob(pattern, cwd) {
137
+ const absolute = pattern.startsWith("/") ? pattern : `${cwd}/${pattern}`;
138
+ const wildcard = absolute.search(/[*?[{]/);
139
+ const cut = wildcard === -1
140
+ ? absolute.lastIndexOf("/")
141
+ : absolute.lastIndexOf("/", wildcard);
142
+ return {
143
+ dir: cut <= 0 ? "/" : absolute.slice(0, cut),
144
+ rest: absolute.slice(cut + 1)
145
+ };
146
+ }
147
+ /**
148
+ * @param runtime The running sub-agent's `runtime()`, read per call — how a
149
+ * sub-agent's file tools reach the workspace its parent prepared. A parent's
150
+ * is `undefined`, and {@link ComputerConfig.workspaceName} names it.
151
+ */
152
+ export function computerWorkspace(config, runtime) {
153
+ const cwd = config.cwd ?? "/workspace";
154
+ // Resolved per call, never memoized: the name belongs to the turn, and a
155
+ // stale one would route this caller's writes into another's files.
156
+ const named = () => workspaceNameFromRuntime(runtime?.()) ?? config.workspaceName();
157
+ const host = (name) => config.binding.get(config.binding.idFromName(name));
158
+ /**
159
+ * One call, against the workspace this turn names. Opened without starting a
160
+ * container — see `WorkspaceHost.__getWorkspaceFsStub`.
161
+ */
162
+ const withFs = async (path, verb, body) => {
163
+ const env_1 = { stack: [], error: void 0, hasError: false };
164
+ try {
165
+ const refusal = guardPath(path, verb);
166
+ if (refusal)
167
+ throw new Error(refusal);
168
+ const name = named();
169
+ const ws = __addDisposableResource(env_1, await openWorkspaceFs(host(name)), false);
170
+ return await body(ws.fs, name);
171
+ }
172
+ catch (e_1) {
173
+ env_1.error = e_1;
174
+ env_1.hasError = true;
175
+ }
176
+ finally {
177
+ __disposeResources(env_1);
178
+ }
179
+ };
180
+ /**
181
+ * A write, refused while the workspace cannot keep it. Thrown rather than
182
+ * returned: Think's tools report a thrown error to the model, and a write
183
+ * that returned would read as one that landed.
184
+ */
185
+ const withWritableFs = (path, verb, body) => withFs(path, verb, async (fs, name) => {
186
+ // Fails open, for the reason the tools' own write gate does: a read of
187
+ // another Durable Object's state must not take out a working tool.
188
+ const lost = await host(name)
189
+ .advisories()
190
+ .then(writeGate, () => undefined);
191
+ if (lost)
192
+ throw new Error(lost);
193
+ return body(fs, name);
194
+ });
195
+ const workspace = {
196
+ readFile: (path) =>
197
+ // Whole, never truncated: Think's `edit` writes back what it read.
198
+ withFs(path, "read", (fs) => orNull(fs.readFile(path, "utf8"))),
199
+ readFileBytes: (path) => withFs(path, "read", async (fs) => {
200
+ const stream = await orNull(fs.readFile(path));
201
+ return stream === null ? null : drain(stream);
202
+ }),
203
+ stat: (path) => withFs(path, "read", async (fs) => {
204
+ const stat = await statOrNull(fs, path);
205
+ return stat === null ? null : fileInfo({ ...stat, path });
206
+ }),
207
+ readDir: (dir = cwd, opts) => withFs(dir, "list", async (fs) => (await fs.readdir(dir, opts)).map((entry) => fileInfo({
208
+ ...entry,
209
+ path: dir === "/" ? `/${entry.name}` : `${dir}/${entry.name}`
210
+ }))),
211
+ // Pruned rather than filtered: at a repository root either skipped
212
+ // directory outnumbers everything else.
213
+ glob: (pattern) => {
214
+ const { dir, rest } = splitGlob(pattern, cwd);
215
+ return withFs(dir, "find", async (fs) => (await fs.find(dir, rest, {
216
+ limit: GLOB_LIMIT,
217
+ exclude: WALK_SKIPS.map((segment) => `**/${segment}`)
218
+ })).map((entry) => fileInfo({ path: entry.path, isDirectory: entry.type === "dir" })));
219
+ },
220
+ // Locked per file, so a write never lands inside the `edit` tool's read and
221
+ // write of the same file — see `./file-lock.ts`.
222
+ writeFile: (path, content) => withWritableFs(path, "write", (fs, name) => withFileLock(name, path, () => fs.writeFile(path, content))),
223
+ mkdir: (path, opts) => withWritableFs(path, "write", (fs) => fs.mkdir(path, opts)),
224
+ // Locked too: a delete landing between an edit's read and its write would
225
+ // be undone by the write, and both would report success.
226
+ rm: (path, opts) => withWritableFs(path, "delete", (fs, name) => withFileLock(name, path, () => fs.rm(path, opts)))
227
+ // No `writeFileBytes`. Think writes through it on its own account —
228
+ // evicted media, projected skills — and none of that belongs in a
229
+ // repository's checkout. Without it Think skips both, with a warning.
230
+ };
231
+ const route = { name: named, host };
232
+ Object.defineProperty(workspace, ROUTE, { value: route });
233
+ return workspace;
234
+ }
@@ -1,28 +1,10 @@
1
1
  import type { WorkspaceClient, WorkspaceRuntimeStatus } from "@cloudflare/computer";
2
- /**
3
- * Middle-out truncation, so both the first error and the final summary survive.
4
- *
5
- * The guard is not defensive padding. Without it a `max` at or below the marker's
6
- * own length makes `half` zero or negative, and `slice(-0)` is `slice(0)` — the
7
- * *whole* string — so the function returns more than it was given: 500 characters
8
- * in, 543 out at `max: 80`. A silent inversion of the one thing it does,
9
- * reachable from a public config field.
10
- */
11
- export declare function truncateOutput(text: string, max: number): string;
12
- /**
13
- * A byte count in the form a model can act on.
14
- *
15
- * A listing that says a file is 4.2 MB tells the model the read it is about to do
16
- * comes back with a hole in the middle — the one thing it cannot infer from the
17
- * truncated result itself.
18
- */
19
- export declare function humanBytes(bytes: number): string;
20
2
  /**
21
3
  * One `fs.grep` hit, derived from the client rather than imported.
22
4
  *
23
5
  * `@cloudflare/computer` declares `WorkspaceGrepMatch` but does not re-export it,
24
6
  * so there is no name to import. Reading it back off the method keeps this correct
25
- * by construction — the same structural approach {@link readBounded} takes to `fs`.
7
+ * by construction.
26
8
  */
27
9
  type GrepMatch = Awaited<ReturnType<WorkspaceClient["fs"]["grep"]>>[number];
28
10
  /**
@@ -70,8 +52,6 @@ export declare function renderGrepMatches(matches: GrepMatch[], maxChars: number
70
52
  shown: number;
71
53
  capped: boolean;
72
54
  };
73
- /** Elapsed time in the shape a sentence wants. */
74
- export declare function humanMs(ms: number): string;
75
55
  /**
76
56
  * Render a completed command the way a terminal would, minus the noise.
77
57
  *
@@ -100,24 +80,33 @@ export declare function renderResult(result: {
100
80
  stdout: string;
101
81
  stderr: string;
102
82
  status?: WorkspaceRuntimeStatus;
83
+ sync?: {
84
+ status: "complete" | "pending";
85
+ };
103
86
  }, maxChars: number): string;
104
87
  /**
105
- * Why a command has nothing to show for itself.
106
- *
107
- * {@link renderResult} states a non-`completed` status on its verdict line;
108
- * {@link computerExec} has no verdict line, only the four fields its caller
109
- * branches on. A *killed* process writes nothing, so without this a `git clone`
110
- * that hit the ten-minute ceiling reaches the model through
111
- * `@dynamicagents/plugins/repo` as `clone failed:` with an empty reason — which reads
112
- * like a bug in the plugin rather than a limit it can work within.
113
- *
114
- * Only `cancelled`. A `failed` status is an ordinary non-zero exit, where the
115
- * command's own stderr is the better explanation and this would be noise on top
116
- * of it.
117
- *
118
- * Both plausible causes are named because the exit code cannot separate them:
119
- * 137 is SIGKILL, which is what the timeout sends *and* what the kernel sends a
120
- * container that ran out of memory.
88
+ * The command ran; its changes have not landed in the workspace yet.
89
+ *
90
+ * Every command is bracketed by a sync, and the pull afterwards is what moves
91
+ * what it wrote from the container into the Durable Object. That pull can fail
92
+ * while the command itself succeeds — a transport that dropped, a container
93
+ * replaced underneath — and the runtime reports it in `sync` rather than in the
94
+ * exit code, because the command genuinely did run.
95
+ *
96
+ * Worth a sentence to the model because the *next* thing it does is usually read
97
+ * what it just wrote, and the file tools read the workspace rather than the
98
+ * container. Without this the file looks unchanged and the obvious conclusion is
99
+ * that the command did not work, which is the one conclusion that is wrong.
100
+ *
101
+ * Recovery is deliberately not the model's, and is not automatic either: the
102
+ * runtime schedules no retry of its own, so an outstanding pull lands when the
103
+ * host drives one or when a later command's own bracket carries it. What the
104
+ * model is told therefore stops short of "it will be there in a moment" — it is
105
+ * told the read may be stale, that re-running the command is the wrong fix
106
+ * because it would repeat the side effects, and to say so in its result if the
107
+ * workspace still disagrees with what it wrote.
121
108
  */
122
- export declare function cancelledNote(status: WorkspaceRuntimeStatus | undefined, exitCode: number, timeoutMs: number): string | undefined;
109
+ export declare function syncPendingNote(sync: {
110
+ status: "complete" | "pending";
111
+ } | undefined): string | undefined;
123
112
  export {};