@foldrun/core 1.4.2 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/index.d.ts +0 -3
  2. package/dist/index.js +0 -3
  3. package/dist/src/catalog.d.ts +7 -0
  4. package/dist/src/catalog.js +16 -1
  5. package/dist/src/deploy.d.ts +6 -27
  6. package/dist/src/deploy.js +14 -56
  7. package/dist/src/history.d.ts +30 -10
  8. package/dist/src/history.js +232 -99
  9. package/dist/src/library.d.ts +0 -13
  10. package/dist/src/library.js +0 -50
  11. package/dist/src/model-credential.js +1 -1
  12. package/dist/src/model-env.js +1 -1
  13. package/dist/src/platform.d.ts +1 -3
  14. package/dist/src/platform.js +0 -1
  15. package/dist/src/run-container.d.ts +31 -5
  16. package/dist/src/run-container.js +71 -3
  17. package/dist/src/runner.d.ts +2 -0
  18. package/dist/src/runner.js +20 -12
  19. package/dist/src/secrets.js +1 -12
  20. package/dist/src/starter.js +2 -2
  21. package/dist/src/step-exec.d.ts +23 -5
  22. package/dist/src/step-exec.js +30 -23
  23. package/dist/src/store.d.ts +61 -22
  24. package/dist/src/store.js +105 -25
  25. package/dist/src/tool-loop.d.ts +4 -1
  26. package/dist/src/tool-loop.js +2 -0
  27. package/dist/src/translator.d.ts +12 -0
  28. package/dist/src/translator.js +36 -13
  29. package/index.ts +0 -3
  30. package/package.json +1 -3
  31. package/src/catalog.ts +18 -2
  32. package/src/deploy.ts +13 -74
  33. package/src/history.ts +230 -108
  34. package/src/library.ts +0 -44
  35. package/src/model-credential.ts +1 -1
  36. package/src/model-env.ts +1 -1
  37. package/src/platform.ts +1 -4
  38. package/src/run-container.ts +78 -5
  39. package/src/runner.ts +22 -12
  40. package/src/secrets.ts +1 -11
  41. package/src/starter.ts +2 -2
  42. package/src/step-exec.ts +44 -27
  43. package/src/store.ts +145 -37
  44. package/src/tool-loop.ts +6 -1
  45. package/src/translator.ts +39 -15
  46. package/dist/src/git-hook.d.ts +0 -1
  47. package/dist/src/git-hook.js +0 -70
  48. package/dist/src/git.d.ts +0 -37
  49. package/dist/src/git.js +0 -101
  50. package/dist/src/gitrepo.d.ts +0 -113
  51. package/dist/src/gitrepo.js +0 -404
  52. package/dist/src/tar.d.ts +0 -30
  53. package/dist/src/tar.js +0 -112
  54. package/src/git-hook.ts +0 -73
  55. package/src/git.ts +0 -117
  56. package/src/gitrepo.ts +0 -439
  57. package/src/tar.ts +0 -125
package/dist/index.d.ts CHANGED
@@ -24,7 +24,6 @@ export * from "./src/tool-names.ts";
24
24
  export * from "./src/history.ts";
25
25
  export * from "./src/zip.ts";
26
26
  export * from "./src/package.ts";
27
- export * from "./src/gitrepo.ts";
28
27
  export * from "./src/storage.ts";
29
28
  export * from "./src/secrets.ts";
30
29
  export * from "./src/oauth-presets.ts";
@@ -35,5 +34,3 @@ export * from "./src/approvals.ts";
35
34
  export * from "./src/deploy.ts";
36
35
  export * from "./src/agent-import.ts";
37
36
  export * from "./src/account.ts";
38
- export * from "./src/tar.ts";
39
- export * from "./src/git.ts";
package/dist/index.js CHANGED
@@ -35,7 +35,6 @@ export * from "./src/tool-names.js";
35
35
  export * from "./src/history.js";
36
36
  export * from "./src/zip.js";
37
37
  export * from "./src/package.js";
38
- export * from "./src/gitrepo.js";
39
38
  export * from "./src/storage.js";
40
39
  export * from "./src/secrets.js";
41
40
  export * from "./src/oauth-presets.js";
@@ -46,5 +45,3 @@ export * from "./src/approvals.js";
46
45
  export * from "./src/deploy.js";
47
46
  export * from "./src/agent-import.js";
48
47
  export * from "./src/account.js";
49
- export * from "./src/tar.js";
50
- export * from "./src/git.js";
@@ -83,7 +83,14 @@ export declare function clampEffort(catalog: Catalog | null, modelId: string, ef
83
83
  export declare function catalogCost(catalog: Catalog | null, modelId: string, usage: {
84
84
  inputTokens: number;
85
85
  outputTokens: number;
86
+ cacheReadTokens?: number;
87
+ cacheWriteTokens?: number;
86
88
  }): number | null;
89
+ /** Anthropic's cache multipliers over the input price: a write costs a
90
+ * quarter more than fresh input, a read a tenth of it. One source for the
91
+ * mid-step ceiling (priceTurn) and the record's repricing (catalogCost). */
92
+ export declare const CACHE_WRITE_MULTIPLIER = 1.25;
93
+ export declare const CACHE_READ_MULTIPLIER = 0.1;
87
94
  /** Catalogue entries as completion items: price per MTok, context, and the
88
95
  * one capability that decides agent-worthiness. Tool-capable models first —
89
96
  * on an agent platform they are almost always what the author wants. */
@@ -200,8 +200,23 @@ export function catalogCost(catalog, modelId, usage) {
200
200
  const m = findModel(catalog, modelId);
201
201
  if (!m || m.promptPrice == null || m.completionPrice == null)
202
202
  return null;
203
- return usage.inputTokens * m.promptPrice + usage.outputTokens * m.completionPrice;
203
+ // A catalogue quotes one input price; cache traffic is priced off it with
204
+ // the multipliers every gateway that bills for caching has copied from
205
+ // Anthropic. Before the counts were split (18 Oct 2026) every cached
206
+ // token was billed here at the full rate — a long cached step cost ten
207
+ // times its price on the ledger.
208
+ // inputTokens is all input; the cache fields are parts of it.
209
+ const read = usage.cacheReadTokens ?? 0;
210
+ const written = usage.cacheWriteTokens ?? 0;
211
+ const fresh = Math.max(0, usage.inputTokens - read - written);
212
+ const input = fresh + written * CACHE_WRITE_MULTIPLIER + read * CACHE_READ_MULTIPLIER;
213
+ return input * m.promptPrice + usage.outputTokens * m.completionPrice;
204
214
  }
215
+ /** Anthropic's cache multipliers over the input price: a write costs a
216
+ * quarter more than fresh input, a read a tenth of it. One source for the
217
+ * mid-step ceiling (priceTurn) and the record's repricing (catalogCost). */
218
+ export const CACHE_WRITE_MULTIPLIER = 1.25;
219
+ export const CACHE_READ_MULTIPLIER = 0.1;
205
220
  /** Catalogue entries as completion items: price per MTok, context, and the
206
221
  * one capability that decides agent-worthiness. Tool-capable models first —
207
222
  * on an agent platform they are almost always what the author wants. */
@@ -97,7 +97,9 @@ export declare function deployIssues(files: DeployFile[]): DeployIssue[];
97
97
  */
98
98
  export declare function runsInFlight(tenant: string, workspace: string): string[];
99
99
  /** What a deploy would do, without doing it. */
100
- export declare function planDeploy(tenant: string, workspace: string, files: DeployFile[]): DeployPlan;
100
+ export declare function planDeploy(tenant: string, workspace: string, files: DeployFile[], opts?: {
101
+ keepUntracked?: boolean;
102
+ }): DeployPlan;
101
103
  export interface DeployOptions {
102
104
  /** The commit these files came from, recorded so the dashboard can show it. */
103
105
  commit?: string | null;
@@ -115,6 +117,9 @@ export interface DeployOptions {
115
117
  * no window between the check and the swap. Absent: no check, as before.
116
118
  */
117
119
  expectRemoved?: string[];
120
+ /** Keep what is not editable source (planDeploy, saveWorkspace): a
121
+ * restore takes history's tree, and history tracks source only. */
122
+ keepUntracked?: boolean;
118
123
  }
119
124
  /**
120
125
  * Deploy a file list to a workspace, if it checks out.
@@ -124,32 +129,6 @@ export interface DeployOptions {
124
129
  * webhook or a CLI that needs to report *why*, and every reason is already in
125
130
  * the plan.
126
131
  */
127
- /** What a push recorded about itself, where the Repository page reads it. */
128
- export interface PushNote {
129
- commit: string;
130
- at: string;
131
- applied: boolean;
132
- issues?: unknown[];
133
- blockedBy?: string[];
134
- /** Set when a blocked push was applied later, by the retry. */
135
- appliedAt?: string;
136
- }
137
- export declare const pushNotePath: (tenant: string, workspace: string) => string;
138
- export declare function readPushNote(tenant: string, workspace: string): PushNote | null;
139
- export declare function writePushNote(tenant: string, workspace: string, note: PushNote): void;
140
- /**
141
- * A push whose deploy was refused only because a run was in flight is not a
142
- * failure — it is a deploy that has not happened yet. Git already said the
143
- * push succeeded (the deploy happens after the client is gone), so nothing
144
- * would ever tell anyone the workspace on disk is behind `main`. This applies
145
- * it once the runs are done.
146
- *
147
- * Only `blockedBy`: a push refused for `issues` is a broken workspace and
148
- * wants a person, not a retry.
149
- */
150
- export declare function applyPendingPush(tenant: string, workspace: string, filesAt: (tenant: string, scope: string, ref: string) => DeployFile[]): {
151
- commit: string;
152
- } | null;
153
132
  export declare function deployWorkspace(tenant: string, workspace: string, files: DeployFile[], opts?: DeployOptions): DeployResult;
154
133
  export declare function writeDeployedCommit(tenant: string, workspace: string, commit: string): void;
155
134
  export declare function deployedCommit(tenant: string, workspace: string): {
@@ -20,7 +20,7 @@
20
20
  import fs from "node:fs";
21
21
  import os from "node:os";
22
22
  import path from "node:path";
23
- import { saveWorkspace, keptOnDeploy, workspaceDir, listRuns, parseFlow, parseToolDef, WORKSPACE_DIRS, assertSafeName, notifyWorkspaceChanged, } from "./store.js";
23
+ import { saveWorkspace, keptOnDeploy, workspaceDir, listRunSummaries, listWorkspaceFiles, parseFlow, parseToolDef, WORKSPACE_DIRS, assertSafeName, } from "./store.js";
24
24
  import { lintFlow } from "./flow-lint.js";
25
25
  import { refNames } from "./refs.js";
26
26
  import matter from "gray-matter";
@@ -32,7 +32,6 @@ import { networkProblems } from "./network.js";
32
32
  import { actionProblems } from "./web-actions.js";
33
33
  import { languageProblem } from "./language.js";
34
34
  import { localeProblems } from "./locale.js";
35
- import { repoDir } from "./gitrepo.js";
36
35
  import { entriesNoFollow } from "./paths.js";
37
36
  import { filterIgnored, findingFingerprint, ruleForWhere, scanFiles } from "./scan.js";
38
37
  /** Directories a deploy never reads out of a source tree. */
@@ -434,12 +433,12 @@ export function deployIssues(files) {
434
433
  export function runsInFlight(tenant, workspace) {
435
434
  if (!fs.existsSync(workspaceDir(tenant, workspace)))
436
435
  return [];
437
- return listRuns(tenant, workspace)
436
+ return listRunSummaries(tenant, workspace)
438
437
  .filter((r) => r.status === "queued" || r.status === "running" || r.status === "awaiting-approval")
439
438
  .map((r) => r.id);
440
439
  }
441
440
  /** What a deploy would do, without doing it. */
442
- export function planDeploy(tenant, workspace, files) {
441
+ export function planDeploy(tenant, workspace, files, opts = {}) {
443
442
  const dir = workspaceDir(tenant, workspace);
444
443
  const shipped = new Map(files.map((f) => [path.normalize(f.path), f.content]));
445
444
  const present = new Set();
@@ -481,7 +480,10 @@ export function planDeploy(tenant, workspace, files) {
481
480
  // memory, the platform's own files. Reporting these as removals would be
482
481
  // a lie, and the alarming kind; one predicate serves both.
483
482
  const shippedPaths = new Set(shipped.keys());
484
- const removed = [...present].filter((rel) => !shipped.has(rel) && !keptOnDeploy(rel, shippedPaths));
483
+ // keepUntracked (a restore): what is not editable source stays, as
484
+ // saveWorkspace keeps it — see its meta.keepUntracked.
485
+ const tracked = opts.keepUntracked ? new Set(listWorkspaceFiles(tenant, workspace)) : null;
486
+ const removed = [...present].filter((rel) => !shipped.has(rel) && !keptOnDeploy(rel, shippedPaths) && (!tracked || tracked.has(rel)));
485
487
  return {
486
488
  files,
487
489
  added: added.sort(),
@@ -492,62 +494,18 @@ export function planDeploy(tenant, workspace, files) {
492
494
  blockedBy: runsInFlight(tenant, workspace),
493
495
  };
494
496
  }
495
- export const pushNotePath = (tenant, workspace) => path.join(repoDir(tenant, workspace), "foldrun-last-push.json");
496
- export function readPushNote(tenant, workspace) {
497
- try {
498
- return JSON.parse(fs.readFileSync(pushNotePath(tenant, workspace), "utf8"));
499
- }
500
- catch {
501
- return null;
502
- }
503
- }
504
- export function writePushNote(tenant, workspace, note) {
505
- try {
506
- fs.writeFileSync(pushNotePath(tenant, workspace), JSON.stringify(note));
507
- }
508
- catch {
509
- // The note is a record, not the deploy. Losing it must not fail a push.
510
- }
511
- // A push waiting on a run is the scheduler's to apply; tell it there is one.
512
- notifyWorkspaceChanged(tenant, workspace, "push");
513
- }
514
497
  /**
515
- * A push whose deploy was refused only because a run was in flight is not a
516
- * failure — it is a deploy that has not happened yet. Git already said the
517
- * push succeeded (the deploy happens after the client is gone), so nothing
518
- * would ever tell anyone the workspace on disk is behind `main`. This applies
519
- * it once the runs are done.
498
+ * Deploy a file list to a workspace, if it checks out.
520
499
  *
521
- * Only `blockedBy`: a push refused for `issues` is a broken workspace and
522
- * wants a person, not a retry.
500
+ * Returns the plan either way, with `applied` saying whether anything changed.
501
+ * A refused deploy is a normal outcome, not an exception: the caller is a
502
+ * webhook or a CLI that needs to report *why*, and every reason is already in
503
+ * the plan.
523
504
  */
524
- export function applyPendingPush(tenant, workspace, filesAt) {
525
- const note = readPushNote(tenant, workspace);
526
- if (!note || note.applied)
527
- return null;
528
- if (!note.blockedBy?.length || (note.issues?.length ?? 0) > 0)
529
- return null;
530
- if (runsInFlight(tenant, workspace).length > 0)
531
- return null;
532
- // Only the commit the note names, and only while it is still what main
533
- // points at — a newer push supersedes it and wrote its own note.
534
- const result = deployWorkspace(tenant, workspace, filesAt(tenant, workspace, note.commit), {
535
- commit: note.commit,
536
- force: false,
537
- });
538
- writePushNote(tenant, workspace, {
539
- ...note,
540
- applied: result.applied,
541
- issues: result.issues ?? [],
542
- blockedBy: result.blockedBy ?? [],
543
- ...(result.applied ? { appliedAt: new Date().toISOString() } : {}),
544
- });
545
- return result.applied ? { commit: note.commit } : null;
546
- }
547
505
  export function deployWorkspace(tenant, workspace, files, opts = {}) {
548
506
  assertSafeName(tenant, "tenant");
549
507
  assertSafeName(workspace, "workspace");
550
- const plan = planDeploy(tenant, workspace, files);
508
+ const plan = planDeploy(tenant, workspace, files, { keepUntracked: opts.keepUntracked });
551
509
  const blocked = plan.blockedBy.length > 0 && !opts.force;
552
510
  if (plan.issues.length > 0 || blocked) {
553
511
  return { ...plan, applied: false, commit: null, preserved: 0 };
@@ -559,7 +517,7 @@ export function deployWorkspace(tenant, workspace, files, opts = {}) {
559
517
  return { ...plan, applied: false, commit: null, preserved: 0, unexpectedRemovals };
560
518
  }
561
519
  }
562
- const { preserved } = saveWorkspace(tenant, workspace, files, { commit: opts.commit ?? null, by: opts.by ?? "deploy", message: opts.message });
520
+ const { preserved } = saveWorkspace(tenant, workspace, files, { commit: opts.commit ?? null, by: opts.by ?? "deploy", message: opts.message, keepUntracked: opts.keepUntracked });
563
521
  const commit = opts.commit ?? null;
564
522
  if (commit)
565
523
  writeDeployedCommit(tenant, workspace, commit);
@@ -9,11 +9,6 @@
9
9
  */
10
10
  type TreeReader = (tenant: string, scope: string) => RevisionFile[] | null;
11
11
  export declare function registerTreeReader(fn: TreeReader): void;
12
- /** Something to do after a commit lands on main — the mirror push. Registered
13
- * by mirror.ts; this module cannot import it (secrets, settings). */
14
- type AfterCommit = (tenant: string, scope: string, sha: string) => void;
15
- export declare function registerAfterCommit(fn: AfterCommit): void;
16
- export declare function notifyCommit(tenant: string, scope: string, sha: string): void;
17
12
  export interface RevisionFile {
18
13
  path: string;
19
14
  /** Null when the file did not exist before. */
@@ -55,17 +50,42 @@ export declare function recordRevision(tenant: string, scope: string, files: Rev
55
50
  id?: string;
56
51
  commit?: string | null;
57
52
  }): Revision | null;
53
+ /** Whether a revision can be restored to: it has a snapshot. */
54
+ export declare function hasSnapshot(tenant: string, scope: string, id: string): boolean;
55
+ /** The whole tree at revision `id`, or null when it has no snapshot. */
56
+ export declare function snapshotAt(tenant: string, scope: string, id: string): {
57
+ path: string;
58
+ content: string;
59
+ }[] | null;
60
+ /** The newest revision that can be restored to — what "now" is in history. */
61
+ export declare function headRevision(tenant: string, scope: string): RevisionSummary | null;
62
+ /**
63
+ * A scope with no snapshot yet gets one: its tree as it stands, recorded as
64
+ * a revision of its own — where its history starts. Every workspace that
65
+ * existed when history moved off git starts here, and so does one created
66
+ * by a path that never recorded a change. Returns the head's id, or null
67
+ * when there is nothing to snapshot.
68
+ */
69
+ export declare function ensureBaseline(tenant: string, scope: string, by?: string): string | null;
70
+ /**
71
+ * The revision a restore means: an id (or the commit a deploy recorded), a
72
+ * date (`2026-10-17`, `2026-10-17T09:00Z`) or a span back from now (`90m`,
73
+ * `12h`, `3d`, `2w`) — the newest revision with a snapshot at or before that
74
+ * moment. Null when history holds nothing that old. Throws (status 400) on
75
+ * something that is none of those.
76
+ */
77
+ export declare function resolveRevision(tenant: string, scope: string, to: string, now?: number): {
78
+ id: string;
79
+ at: string;
80
+ by: string;
81
+ message: string;
82
+ } | null;
58
83
  /** Newest first. `forPath` narrows to revisions that touched one file. */
59
84
  export declare function listRevisions(tenant: string, scope: string, opts?: {
60
85
  forPath?: string;
61
86
  limit?: number;
62
87
  }): RevisionSummary[];
63
88
  export declare function readRevision(tenant: string, scope: string, id: string, forPath?: string): Revision | null;
64
- /**
65
- * Make sure a scope's repository has its initial import, so a clone made
66
- * right now is complete. A no-op once any commit exists.
67
- */
68
- export declare function ensureImported(tenant: string, scope: string): string | null;
69
89
  /** The most recent revision id for a scope — what a change is attributed
70
90
  * to when nothing more specific (a deployed commit) is on record. */
71
91
  export declare function latestRevisionId(tenant: string, scope: string): string | null;