toolcraft 0.0.5 → 0.0.7

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 (149) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +77 -59
  4. package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.d.ts +2 -0
  5. package/node_modules/@poe-code/agent-defs/dist/agents/claude-code.js +15 -0
  6. package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.d.ts +2 -0
  7. package/node_modules/@poe-code/agent-defs/dist/agents/claude-desktop.js +13 -0
  8. package/node_modules/@poe-code/agent-defs/dist/agents/codex.d.ts +2 -0
  9. package/node_modules/@poe-code/agent-defs/dist/agents/codex.js +14 -0
  10. package/node_modules/@poe-code/agent-defs/dist/agents/goose.d.ts +2 -0
  11. package/node_modules/@poe-code/agent-defs/dist/agents/goose.js +14 -0
  12. package/node_modules/@poe-code/agent-defs/dist/agents/index.d.ts +7 -0
  13. package/node_modules/@poe-code/agent-defs/dist/agents/index.js +7 -0
  14. package/node_modules/@poe-code/agent-defs/dist/agents/kimi.d.ts +2 -0
  15. package/node_modules/@poe-code/agent-defs/dist/agents/kimi.js +15 -0
  16. package/node_modules/@poe-code/agent-defs/dist/agents/opencode.d.ts +2 -0
  17. package/node_modules/@poe-code/agent-defs/dist/agents/opencode.js +14 -0
  18. package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.d.ts +2 -0
  19. package/node_modules/@poe-code/agent-defs/dist/agents/poe-agent.js +13 -0
  20. package/node_modules/@poe-code/agent-defs/dist/index.d.ts +5 -0
  21. package/node_modules/@poe-code/agent-defs/dist/index.js +3 -0
  22. package/node_modules/@poe-code/agent-defs/dist/registry.d.ts +3 -0
  23. package/node_modules/@poe-code/agent-defs/dist/registry.js +26 -0
  24. package/node_modules/@poe-code/agent-defs/dist/specifier.d.ts +7 -0
  25. package/node_modules/@poe-code/agent-defs/dist/specifier.js +27 -0
  26. package/node_modules/@poe-code/agent-defs/dist/types.d.ts +16 -0
  27. package/node_modules/@poe-code/agent-defs/dist/types.js +1 -0
  28. package/node_modules/@poe-code/agent-defs/package.json +20 -0
  29. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.d.ts +5 -0
  30. package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +552 -0
  31. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.d.ts +17 -0
  32. package/node_modules/@poe-code/config-mutations/dist/execution/path-utils.js +58 -0
  33. package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.d.ts +7 -0
  34. package/node_modules/@poe-code/config-mutations/dist/execution/run-mutations.js +46 -0
  35. package/node_modules/@poe-code/config-mutations/dist/formats/index.d.ts +13 -0
  36. package/node_modules/@poe-code/config-mutations/dist/formats/index.js +49 -0
  37. package/node_modules/@poe-code/config-mutations/dist/formats/json.d.ts +31 -0
  38. package/node_modules/@poe-code/config-mutations/dist/formats/json.js +140 -0
  39. package/node_modules/@poe-code/config-mutations/dist/formats/toml.d.ts +2 -0
  40. package/node_modules/@poe-code/config-mutations/dist/formats/toml.js +72 -0
  41. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.d.ts +2 -0
  42. package/node_modules/@poe-code/config-mutations/dist/formats/yaml.js +73 -0
  43. package/node_modules/@poe-code/config-mutations/dist/fs-utils.d.ts +18 -0
  44. package/node_modules/@poe-code/config-mutations/dist/fs-utils.js +45 -0
  45. package/node_modules/@poe-code/config-mutations/dist/index.d.ts +8 -0
  46. package/node_modules/@poe-code/config-mutations/dist/index.js +8 -0
  47. package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.d.ts +47 -0
  48. package/node_modules/@poe-code/config-mutations/dist/mutations/config-mutation.js +34 -0
  49. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.d.ts +52 -0
  50. package/node_modules/@poe-code/config-mutations/dist/mutations/file-mutation.js +46 -0
  51. package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.d.ts +40 -0
  52. package/node_modules/@poe-code/config-mutations/dist/mutations/template-mutation.js +32 -0
  53. package/node_modules/@poe-code/config-mutations/dist/template/render.d.ts +7 -0
  54. package/node_modules/@poe-code/config-mutations/dist/template/render.js +28 -0
  55. package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.d.ts +7 -0
  56. package/node_modules/@poe-code/config-mutations/dist/testing/format-utils.js +21 -0
  57. package/node_modules/@poe-code/config-mutations/dist/testing/index.d.ts +3 -0
  58. package/node_modules/@poe-code/config-mutations/dist/testing/index.js +2 -0
  59. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.d.ts +25 -0
  60. package/node_modules/@poe-code/config-mutations/dist/testing/mock-fs.js +170 -0
  61. package/node_modules/@poe-code/config-mutations/dist/types.d.ts +156 -0
  62. package/node_modules/@poe-code/config-mutations/dist/types.js +6 -0
  63. package/node_modules/@poe-code/config-mutations/package.json +33 -0
  64. package/node_modules/@poe-code/file-lock/README.md +52 -0
  65. package/node_modules/@poe-code/file-lock/dist/index.d.ts +1 -0
  66. package/node_modules/@poe-code/file-lock/dist/index.js +1 -0
  67. package/node_modules/@poe-code/file-lock/dist/lock.d.ts +27 -0
  68. package/node_modules/@poe-code/file-lock/dist/lock.js +203 -0
  69. package/node_modules/@poe-code/file-lock/package.json +23 -0
  70. package/node_modules/auth-store/README.md +47 -0
  71. package/node_modules/auth-store/dist/create-secret-store.d.ts +2 -0
  72. package/node_modules/auth-store/dist/create-secret-store.js +35 -0
  73. package/node_modules/auth-store/dist/encrypted-file-store.d.ts +39 -0
  74. package/node_modules/auth-store/dist/encrypted-file-store.js +156 -0
  75. package/node_modules/auth-store/dist/index.d.ts +7 -0
  76. package/node_modules/auth-store/dist/index.js +4 -0
  77. package/node_modules/auth-store/dist/keychain-store.d.ts +22 -0
  78. package/node_modules/auth-store/dist/keychain-store.js +111 -0
  79. package/node_modules/auth-store/dist/provider-store.d.ts +10 -0
  80. package/node_modules/auth-store/dist/provider-store.js +28 -0
  81. package/node_modules/auth-store/dist/types.d.ts +20 -0
  82. package/node_modules/auth-store/dist/types.js +1 -0
  83. package/node_modules/auth-store/package.json +25 -0
  84. package/node_modules/mcp-oauth/README.md +31 -0
  85. package/node_modules/mcp-oauth/dist/client/auth-store-session-store.d.ts +14 -0
  86. package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +97 -0
  87. package/node_modules/mcp-oauth/dist/client/authorization-state.d.ts +8 -0
  88. package/node_modules/mcp-oauth/dist/client/authorization-state.js +34 -0
  89. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.d.ts +3 -0
  90. package/node_modules/mcp-oauth/dist/client/default-oauth-client-provider.js +491 -0
  91. package/node_modules/mcp-oauth/dist/client/loopback-authorization.d.ts +20 -0
  92. package/node_modules/mcp-oauth/dist/client/loopback-authorization.js +169 -0
  93. package/node_modules/mcp-oauth/dist/client/pkce.d.ts +2 -0
  94. package/node_modules/mcp-oauth/dist/client/pkce.js +7 -0
  95. package/node_modules/mcp-oauth/dist/client/token-endpoint.d.ts +40 -0
  96. package/node_modules/mcp-oauth/dist/client/token-endpoint.js +143 -0
  97. package/node_modules/mcp-oauth/dist/client/types.d.ts +113 -0
  98. package/node_modules/mcp-oauth/dist/client/types.js +1 -0
  99. package/node_modules/mcp-oauth/dist/index.d.ts +10 -0
  100. package/node_modules/mcp-oauth/dist/index.js +7 -0
  101. package/node_modules/mcp-oauth/dist/resource-indicator.d.ts +1 -0
  102. package/node_modules/mcp-oauth/dist/resource-indicator.js +11 -0
  103. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.d.ts +27 -0
  104. package/node_modules/mcp-oauth/dist/server/jwks-token-verifier.js +259 -0
  105. package/node_modules/mcp-oauth/dist/types.compile-check.d.ts +1 -0
  106. package/node_modules/mcp-oauth/dist/types.compile-check.js +22 -0
  107. package/node_modules/mcp-oauth/package.json +31 -0
  108. package/node_modules/tiny-mcp-client/.turbo/turbo-build.log +4 -0
  109. package/node_modules/tiny-mcp-client/dist/index.d.ts +2 -0
  110. package/node_modules/tiny-mcp-client/dist/index.js +1 -0
  111. package/node_modules/tiny-mcp-client/dist/internal.d.ts +547 -0
  112. package/node_modules/tiny-mcp-client/dist/internal.js +2404 -0
  113. package/node_modules/tiny-mcp-client/dist/jsonrpc-types.compile-check.d.ts +1 -0
  114. package/node_modules/tiny-mcp-client/dist/jsonrpc-types.compile-check.js +37 -0
  115. package/node_modules/tiny-mcp-client/dist/mcp-lifecycle-types.compile-check.d.ts +1 -0
  116. package/node_modules/tiny-mcp-client/dist/mcp-lifecycle-types.compile-check.js +50 -0
  117. package/node_modules/tiny-mcp-client/dist/mcp-prompt-types.compile-check.d.ts +1 -0
  118. package/node_modules/tiny-mcp-client/dist/mcp-prompt-types.compile-check.js +50 -0
  119. package/node_modules/tiny-mcp-client/dist/mcp-resource-types.compile-check.d.ts +1 -0
  120. package/node_modules/tiny-mcp-client/dist/mcp-resource-types.compile-check.js +51 -0
  121. package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.d.ts +1 -0
  122. package/node_modules/tiny-mcp-client/dist/mcp-tool-types.compile-check.js +89 -0
  123. package/node_modules/tiny-mcp-client/dist/mcp-transport-types.compile-check.d.ts +1 -0
  124. package/node_modules/tiny-mcp-client/dist/mcp-transport-types.compile-check.js +56 -0
  125. package/node_modules/tiny-mcp-client/dist/mcp-utility-types.compile-check.d.ts +1 -0
  126. package/node_modules/tiny-mcp-client/dist/mcp-utility-types.compile-check.js +145 -0
  127. package/node_modules/tiny-mcp-client/dist/oauth-discovery.d.ts +24 -0
  128. package/node_modules/tiny-mcp-client/dist/oauth-discovery.js +385 -0
  129. package/node_modules/tiny-mcp-client/package.json +22 -0
  130. package/node_modules/tiny-mcp-client/src/http-oauth.integration.test.ts +823 -0
  131. package/node_modules/tiny-mcp-client/src/http-oauth.test.ts +882 -0
  132. package/node_modules/tiny-mcp-client/src/index.ts +94 -0
  133. package/node_modules/tiny-mcp-client/src/internal.ts +3566 -0
  134. package/node_modules/tiny-mcp-client/src/jsonrpc-types.compile-check.ts +66 -0
  135. package/node_modules/tiny-mcp-client/src/mcp-client-http-transport.integration.test.ts +222 -0
  136. package/node_modules/tiny-mcp-client/src/mcp-client-sdk.test.ts +1294 -0
  137. package/node_modules/tiny-mcp-client/src/mcp-client-tiny-stdio-test-server-tools.test.ts +143 -0
  138. package/node_modules/tiny-mcp-client/src/mcp-lifecycle-types.compile-check.ts +65 -0
  139. package/node_modules/tiny-mcp-client/src/mcp-prompt-types.compile-check.ts +66 -0
  140. package/node_modules/tiny-mcp-client/src/mcp-resource-types.compile-check.ts +70 -0
  141. package/node_modules/tiny-mcp-client/src/mcp-tool-types.compile-check.ts +117 -0
  142. package/node_modules/tiny-mcp-client/src/mcp-transport-types.compile-check.ts +75 -0
  143. package/node_modules/tiny-mcp-client/src/mcp-utility-types.compile-check.ts +181 -0
  144. package/node_modules/tiny-mcp-client/src/mock-servers.test.ts +980 -0
  145. package/node_modules/tiny-mcp-client/src/oauth-discovery.ts +583 -0
  146. package/node_modules/tiny-mcp-client/src/transports.test.ts +8139 -0
  147. package/node_modules/tiny-mcp-client/src/utilities.test.ts +372 -0
  148. package/node_modules/tiny-mcp-client/tsconfig.json +11 -0
  149. package/package.json +24 -11
@@ -0,0 +1,32 @@
1
+ function write(options) {
2
+ return {
3
+ kind: "templateWrite",
4
+ target: options.target,
5
+ templateId: options.templateId,
6
+ context: options.context,
7
+ label: options.label
8
+ };
9
+ }
10
+ function mergeToml(options) {
11
+ return {
12
+ kind: "templateMergeToml",
13
+ target: options.target,
14
+ templateId: options.templateId,
15
+ context: options.context,
16
+ label: options.label
17
+ };
18
+ }
19
+ function mergeJson(options) {
20
+ return {
21
+ kind: "templateMergeJson",
22
+ target: options.target,
23
+ templateId: options.templateId,
24
+ context: options.context,
25
+ label: options.label
26
+ };
27
+ }
28
+ export const templateMutation = {
29
+ write,
30
+ mergeToml,
31
+ mergeJson
32
+ };
@@ -0,0 +1,7 @@
1
+ export type TemplateVariables = Record<string, string | number | boolean | string[]>;
2
+ /**
3
+ * Render a mustache template with the given variables.
4
+ * Arrays are automatically joined with newlines.
5
+ * HTML escaping is disabled.
6
+ */
7
+ export declare function renderTemplate(template: string, variables: TemplateVariables): string;
@@ -0,0 +1,28 @@
1
+ import Mustache from "mustache";
2
+ // Disable HTML escaping - we're rendering prompts, not HTML
3
+ const originalEscape = Mustache.escape;
4
+ /**
5
+ * Render a mustache template with the given variables.
6
+ * Arrays are automatically joined with newlines.
7
+ * HTML escaping is disabled.
8
+ */
9
+ export function renderTemplate(template, variables) {
10
+ // Pre-process variables to handle arrays
11
+ const processed = {};
12
+ for (const [key, value] of Object.entries(variables)) {
13
+ if (Array.isArray(value)) {
14
+ processed[key] = value.join("\n");
15
+ }
16
+ else {
17
+ processed[key] = value;
18
+ }
19
+ }
20
+ // Temporarily disable HTML escaping
21
+ Mustache.escape = (text) => text;
22
+ try {
23
+ return Mustache.render(template, processed);
24
+ }
25
+ finally {
26
+ Mustache.escape = originalEscape;
27
+ }
28
+ }
@@ -0,0 +1,7 @@
1
+ import type { ConfigObject } from "../types.js";
2
+ export declare function parseToml(content: string): ConfigObject;
3
+ export declare function serializeToml(obj: ConfigObject): string;
4
+ export declare function parseJson(content: string): ConfigObject;
5
+ export declare function serializeJson(obj: ConfigObject): string;
6
+ export declare function parseYaml(content: string): ConfigObject;
7
+ export declare function serializeYaml(obj: ConfigObject): string;
@@ -0,0 +1,21 @@
1
+ import { jsonFormat } from "../formats/json.js";
2
+ import { tomlFormat } from "../formats/toml.js";
3
+ import { yamlFormat } from "../formats/yaml.js";
4
+ export function parseToml(content) {
5
+ return tomlFormat.parse(content);
6
+ }
7
+ export function serializeToml(obj) {
8
+ return tomlFormat.serialize(obj);
9
+ }
10
+ export function parseJson(content) {
11
+ return jsonFormat.parse(content);
12
+ }
13
+ export function serializeJson(obj) {
14
+ return jsonFormat.serialize(obj);
15
+ }
16
+ export function parseYaml(content) {
17
+ return yamlFormat.parse(content);
18
+ }
19
+ export function serializeYaml(obj) {
20
+ return yamlFormat.serialize(obj);
21
+ }
@@ -0,0 +1,3 @@
1
+ export { createMockFs } from "./mock-fs.js";
2
+ export type { MockFileSystem } from "./mock-fs.js";
3
+ export { parseToml, serializeToml, parseJson, serializeJson, parseYaml, serializeYaml } from "./format-utils.js";
@@ -0,0 +1,2 @@
1
+ export { createMockFs } from "./mock-fs.js";
2
+ export { parseToml, serializeToml, parseJson, serializeJson, parseYaml, serializeYaml } from "./format-utils.js";
@@ -0,0 +1,25 @@
1
+ import type { FileSystem } from "../types.js";
2
+ export interface MockFileSystem extends FileSystem {
3
+ /** Current file contents, keyed by absolute path */
4
+ files: Record<string, string>;
5
+ /** Created directories */
6
+ directories: Set<string>;
7
+ /** Check if a path exists (file or directory) */
8
+ exists(path: string): boolean;
9
+ /** Get file content or undefined if not found */
10
+ getContent(path: string): string | undefined;
11
+ /** Read file with encoding overloads for compatibility */
12
+ readFile(path: string, encoding: BufferEncoding): Promise<string>;
13
+ readFile(path: string): Promise<Buffer>;
14
+ }
15
+ export interface MockFsOptions {
16
+ /** Initial files - paths can use ~ which will be expanded to homeDir */
17
+ [path: string]: string;
18
+ }
19
+ /**
20
+ * Create an in-memory mock filesystem for testing mutations.
21
+ *
22
+ * @param initialFiles - Initial files to populate the filesystem with
23
+ * @param homeDir - Home directory for ~ expansion (defaults to /home/test)
24
+ */
25
+ export declare function createMockFs(initialFiles?: MockFsOptions, homeDir?: string): MockFileSystem;
@@ -0,0 +1,170 @@
1
+ import path from "node:path";
2
+ const DEFAULT_HOME_DIR = "/home/test";
3
+ /**
4
+ * Create an in-memory mock filesystem for testing mutations.
5
+ *
6
+ * @param initialFiles - Initial files to populate the filesystem with
7
+ * @param homeDir - Home directory for ~ expansion (defaults to /home/test)
8
+ */
9
+ export function createMockFs(initialFiles, homeDir = DEFAULT_HOME_DIR) {
10
+ const files = {};
11
+ const directories = new Set();
12
+ // Initialize with provided files
13
+ if (initialFiles) {
14
+ for (const [filePath, content] of Object.entries(initialFiles)) {
15
+ const absolutePath = expandPath(filePath, homeDir);
16
+ files[absolutePath] = content;
17
+ // Ensure parent directories exist
18
+ const parentDir = path.dirname(absolutePath);
19
+ addDirectoryTree(parentDir, directories);
20
+ }
21
+ }
22
+ // Ensure home directory exists
23
+ addDirectoryTree(homeDir, directories);
24
+ const mockFs = {
25
+ files,
26
+ directories,
27
+ exists(filePath) {
28
+ const absolutePath = expandPath(filePath, homeDir);
29
+ return absolutePath in files || directories.has(absolutePath);
30
+ },
31
+ getContent(filePath) {
32
+ const absolutePath = expandPath(filePath, homeDir);
33
+ return files[absolutePath];
34
+ },
35
+ async readFile(filePath, encoding) {
36
+ const absolutePath = expandPath(filePath, homeDir);
37
+ if (!(absolutePath in files)) {
38
+ const error = new Error(`ENOENT: no such file or directory, open '${absolutePath}'`);
39
+ error.code = "ENOENT";
40
+ throw error;
41
+ }
42
+ const content = files[absolutePath];
43
+ if (encoding) {
44
+ return content;
45
+ }
46
+ return Buffer.from(content, "utf8");
47
+ },
48
+ async writeFile(filePath, content, options) {
49
+ void options; // TypeScript satisfaction
50
+ const absolutePath = expandPath(filePath, homeDir);
51
+ // Ensure parent directory exists
52
+ const parentDir = path.dirname(absolutePath);
53
+ if (!directories.has(parentDir)) {
54
+ const error = new Error(`ENOENT: no such file or directory, open '${absolutePath}'`);
55
+ error.code = "ENOENT";
56
+ throw error;
57
+ }
58
+ if (typeof content === "string") {
59
+ files[absolutePath] = content;
60
+ }
61
+ else if (Buffer.isBuffer(content)) {
62
+ files[absolutePath] = content.toString("utf8");
63
+ }
64
+ else {
65
+ files[absolutePath] = Buffer.from(content.buffer, content.byteOffset, content.byteLength).toString("utf8");
66
+ }
67
+ },
68
+ async mkdir(dirPath, options) {
69
+ const absolutePath = expandPath(dirPath, homeDir);
70
+ if (options?.recursive) {
71
+ addDirectoryTree(absolutePath, directories);
72
+ }
73
+ else {
74
+ // Check parent exists
75
+ const parentDir = path.dirname(absolutePath);
76
+ if (parentDir !== absolutePath && !directories.has(parentDir)) {
77
+ const error = new Error(`ENOENT: no such file or directory, mkdir '${absolutePath}'`);
78
+ error.code = "ENOENT";
79
+ throw error;
80
+ }
81
+ directories.add(absolutePath);
82
+ }
83
+ },
84
+ async unlink(filePath) {
85
+ const absolutePath = expandPath(filePath, homeDir);
86
+ if (!(absolutePath in files)) {
87
+ const error = new Error(`ENOENT: no such file or directory, unlink '${absolutePath}'`);
88
+ error.code = "ENOENT";
89
+ throw error;
90
+ }
91
+ delete files[absolutePath];
92
+ },
93
+ async stat(filePath) {
94
+ const absolutePath = expandPath(filePath, homeDir);
95
+ if (absolutePath in files) {
96
+ return { mode: 0o644 };
97
+ }
98
+ if (directories.has(absolutePath)) {
99
+ return { mode: 0o755 };
100
+ }
101
+ const error = new Error(`ENOENT: no such file or directory, stat '${absolutePath}'`);
102
+ error.code = "ENOENT";
103
+ throw error;
104
+ },
105
+ async readdir(dirPath) {
106
+ const absolutePath = expandPath(dirPath, homeDir);
107
+ if (absolutePath in files) {
108
+ const error = new Error(`ENOTDIR: not a directory, scandir '${absolutePath}'`);
109
+ error.code = "ENOTDIR";
110
+ throw error;
111
+ }
112
+ if (!directories.has(absolutePath)) {
113
+ const error = new Error(`ENOENT: no such file or directory, scandir '${absolutePath}'`);
114
+ error.code = "ENOENT";
115
+ throw error;
116
+ }
117
+ const entries = new Set();
118
+ for (const filePath of Object.keys(files)) {
119
+ if (path.dirname(filePath) === absolutePath) {
120
+ entries.add(path.basename(filePath));
121
+ }
122
+ }
123
+ for (const dir of directories) {
124
+ if (dir !== absolutePath && path.dirname(dir) === absolutePath) {
125
+ entries.add(path.basename(dir));
126
+ }
127
+ }
128
+ return Array.from(entries);
129
+ },
130
+ async chmod(filePath, mode) {
131
+ void mode; // In mock fs, we don't actually store mode changes
132
+ const absolutePath = expandPath(filePath, homeDir);
133
+ if (!(absolutePath in files) && !directories.has(absolutePath)) {
134
+ const error = new Error(`ENOENT: no such file or directory, chmod '${absolutePath}'`);
135
+ error.code = "ENOENT";
136
+ throw error;
137
+ }
138
+ // Mode change is a no-op in mock fs but we don't throw
139
+ }
140
+ };
141
+ return mockFs;
142
+ }
143
+ /**
144
+ * Expand ~ to homeDir in a path.
145
+ */
146
+ function expandPath(inputPath, homeDir) {
147
+ if (inputPath.startsWith("~/")) {
148
+ return path.join(homeDir, inputPath.slice(2));
149
+ }
150
+ if (inputPath === "~") {
151
+ return homeDir;
152
+ }
153
+ if (inputPath.startsWith("~")) {
154
+ // ~something (not ~/) - treat as relative path from home
155
+ return path.join(homeDir, inputPath.slice(1));
156
+ }
157
+ return inputPath;
158
+ }
159
+ /**
160
+ * Add a directory and all its parent directories to the set.
161
+ */
162
+ function addDirectoryTree(dirPath, directories) {
163
+ const parts = dirPath.split(path.sep).filter(Boolean);
164
+ let current = "/";
165
+ directories.add(current);
166
+ for (const part of parts) {
167
+ current = path.join(current, part);
168
+ directories.add(current);
169
+ }
170
+ }
@@ -0,0 +1,156 @@
1
+ export type ConfigPrimitive = string | number | boolean | null;
2
+ export type ConfigValue = ConfigPrimitive | ConfigObject | ConfigArray | Date;
3
+ export interface ConfigObject {
4
+ [key: string]: ConfigValue;
5
+ }
6
+ export type ConfigArray = ConfigValue[];
7
+ export declare function isConfigObject(value: unknown): value is ConfigObject;
8
+ export interface FileSystem {
9
+ readFile(path: string, encoding: "utf8"): Promise<string>;
10
+ writeFile(path: string, content: string, options?: {
11
+ encoding: "utf8";
12
+ }): Promise<void>;
13
+ mkdir(path: string, options?: {
14
+ recursive: boolean;
15
+ }): Promise<void>;
16
+ unlink(path: string): Promise<void>;
17
+ rm?(path: string, options?: {
18
+ recursive?: boolean;
19
+ force?: boolean;
20
+ }): Promise<void>;
21
+ stat(path: string): Promise<{
22
+ mode?: number;
23
+ }>;
24
+ readdir(path: string): Promise<string[]>;
25
+ chmod?(path: string, mode: number): Promise<void>;
26
+ }
27
+ export type TemplateLoader = (templateId: string) => Promise<string>;
28
+ export interface ConfigFormat {
29
+ /** Parse string content into object */
30
+ parse(content: string): ConfigObject;
31
+ /** Serialize object to string (with consistent formatting) */
32
+ serialize(obj: ConfigObject): string;
33
+ /** Deep merge patch into base, returning new object */
34
+ merge(base: ConfigObject, patch: ConfigObject): ConfigObject;
35
+ /** Remove keys matching shape from object, returning new object */
36
+ prune(obj: ConfigObject, shape: ConfigObject): {
37
+ changed: boolean;
38
+ result: ConfigObject;
39
+ };
40
+ }
41
+ export interface PathMapper {
42
+ /** Map a target directory to a different location (e.g., for isolated configs) */
43
+ mapTargetDirectory(input: {
44
+ targetDirectory: string;
45
+ }): string;
46
+ }
47
+ export interface MutationContext {
48
+ /** Filesystem interface - required */
49
+ fs: FileSystem;
50
+ /** Home directory for ~ expansion - required */
51
+ homeDir: string;
52
+ /** Optional dry-run mode */
53
+ dryRun?: boolean;
54
+ /** Optional observers for logging */
55
+ observers?: MutationObservers;
56
+ /** Required for template mutations */
57
+ templates?: TemplateLoader;
58
+ /** Optional path mapper for redirecting paths (used for isolated configs) */
59
+ pathMapper?: PathMapper;
60
+ }
61
+ export interface MutationOptions {
62
+ [key: string]: unknown;
63
+ }
64
+ export type ValueResolver<T> = T | ((ctx: MutationOptions) => T);
65
+ interface BaseMutation {
66
+ /** Human-readable label for logging */
67
+ label?: string;
68
+ }
69
+ export interface ConfigMergeMutation extends BaseMutation {
70
+ kind: "configMerge";
71
+ target: ValueResolver<string>;
72
+ value: ValueResolver<ConfigObject>;
73
+ format?: "json" | "toml" | "yaml";
74
+ pruneByPrefix?: Record<string, string>;
75
+ }
76
+ export interface ConfigPruneMutation extends BaseMutation {
77
+ kind: "configPrune";
78
+ target: ValueResolver<string>;
79
+ shape: ValueResolver<ConfigObject>;
80
+ format?: "json" | "toml" | "yaml";
81
+ onlyIf?: (doc: ConfigObject, ctx: MutationOptions) => boolean;
82
+ }
83
+ export interface ConfigTransformMutation extends BaseMutation {
84
+ kind: "configTransform";
85
+ target: ValueResolver<string>;
86
+ format?: "json" | "toml" | "yaml";
87
+ transform: (content: ConfigObject, ctx: MutationOptions) => {
88
+ content: ConfigObject | null;
89
+ changed: boolean;
90
+ };
91
+ }
92
+ export interface EnsureDirectoryMutation extends BaseMutation {
93
+ kind: "ensureDirectory";
94
+ path: ValueResolver<string>;
95
+ }
96
+ export interface RemoveDirectoryMutation extends BaseMutation {
97
+ kind: "removeDirectory";
98
+ path: ValueResolver<string>;
99
+ force?: boolean;
100
+ }
101
+ export interface RemoveFileMutation extends BaseMutation {
102
+ kind: "removeFile";
103
+ target: ValueResolver<string>;
104
+ whenEmpty?: boolean;
105
+ whenContentMatches?: RegExp;
106
+ }
107
+ export interface ChmodMutation extends BaseMutation {
108
+ kind: "chmod";
109
+ target: ValueResolver<string>;
110
+ mode: number;
111
+ }
112
+ export interface BackupMutation extends BaseMutation {
113
+ kind: "backup";
114
+ target: ValueResolver<string>;
115
+ }
116
+ export interface TemplateWriteMutation extends BaseMutation {
117
+ kind: "templateWrite";
118
+ target: ValueResolver<string>;
119
+ templateId: string;
120
+ context?: ValueResolver<ConfigObject>;
121
+ }
122
+ export interface TemplateMergeTomlMutation extends BaseMutation {
123
+ kind: "templateMergeToml";
124
+ target: ValueResolver<string>;
125
+ templateId: string;
126
+ context?: ValueResolver<ConfigObject>;
127
+ }
128
+ export interface TemplateMergeJsonMutation extends BaseMutation {
129
+ kind: "templateMergeJson";
130
+ target: ValueResolver<string>;
131
+ templateId: string;
132
+ context?: ValueResolver<ConfigObject>;
133
+ }
134
+ export type Mutation = ConfigMergeMutation | ConfigPruneMutation | ConfigTransformMutation | EnsureDirectoryMutation | RemoveDirectoryMutation | RemoveFileMutation | ChmodMutation | BackupMutation | TemplateWriteMutation | TemplateMergeTomlMutation | TemplateMergeJsonMutation;
135
+ export type MutationEffect = "none" | "mkdir" | "write" | "delete" | "chmod" | "copy";
136
+ export type MutationDetail = "create" | "update" | "delete" | "noop" | "backup";
137
+ export interface MutationOutcome {
138
+ changed: boolean;
139
+ effect: MutationEffect;
140
+ detail?: MutationDetail;
141
+ }
142
+ export interface MutationDetails {
143
+ kind: string;
144
+ label: string;
145
+ targetPath?: string;
146
+ }
147
+ export interface MutationObservers {
148
+ onStart?(details: MutationDetails): void;
149
+ onComplete?(details: MutationDetails, outcome: MutationOutcome): void;
150
+ onError?(details: MutationDetails, error: unknown): void;
151
+ }
152
+ export interface MutationResult {
153
+ changed: boolean;
154
+ effects: MutationOutcome[];
155
+ }
156
+ export {};
@@ -0,0 +1,6 @@
1
+ // ============================================================================
2
+ // Config Object Types
3
+ // ============================================================================
4
+ export function isConfigObject(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@poe-code/config-mutations",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./testing": {
14
+ "types": "./dist/testing/index.d.ts",
15
+ "import": "./dist/testing/index.js"
16
+ }
17
+ },
18
+ "scripts": {
19
+ "build": "tsc"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "dependencies": {
25
+ "jsonc-parser": "^3.3.1",
26
+ "mustache": "^4.2.0",
27
+ "smol-toml": "^1.3.0",
28
+ "yaml": "^2.8.1"
29
+ },
30
+ "devDependencies": {
31
+ "@types/mustache": "^4.2.6"
32
+ }
33
+ }
@@ -0,0 +1,52 @@
1
+ # @poe-code/file-lock
2
+
3
+ Atomic file locking built on lockfile creation with `open(..., "wx")` and timestamped stale cleanup.
4
+
5
+ It creates a sibling lockfile at `<filePath>.lock`, retries with bounded backoff when the lock is already held, and automatically reclaims stale locks. Locks from the current host are reclaimed immediately when their recorded process is no longer running; other locks fall back to the lockfile timestamp and `staleMs`.
6
+
7
+ ## Public API
8
+
9
+ ### `acquireFileLock(filePath, options?)`
10
+
11
+ ```ts
12
+ function acquireFileLock(
13
+ filePath: string,
14
+ options?: FileLockOptions
15
+ ): Promise<ReleaseLock>;
16
+ ```
17
+
18
+ Attempts to acquire an exclusive lock for `filePath` and resolves to an async release function. If the lock already exists, acquisition retries until it succeeds, the lock is considered stale and cleaned up, the provided abort signal fires, or retries are exhausted. When retries are exhausted, it throws `LockTimeoutError`.
19
+
20
+ ## Options
21
+
22
+ | Option | Type | Default | Behavior |
23
+ | --- | --- | --- | --- |
24
+ | `staleMs` | `number` | `1_000` | Lockfile age in milliseconds after which an existing lock is treated as stale and removed when live local PID metadata does not prove the owner is still running. |
25
+ | `retries` | `number` | `20` | Number of retry attempts after the initial acquire attempt. |
26
+ | `minTimeout` | `number` | `25` | Minimum retry backoff in milliseconds. |
27
+ | `maxTimeout` | `number` | `250` | Maximum retry backoff in milliseconds. |
28
+ | `isPidRunning` | `(pid: number) => boolean` | `process.kill(pid, 0)` check | Injectable process liveness check used to reclaim dead local lock owners. |
29
+ | `signal` | `AbortSignal` | none | Cancels acquisition while waiting between retries and rejects with an abort error. |
30
+ | `fs` | `FileLockFs` | `node:fs/promises` adapter | Injectable filesystem used for lock creation, metadata reading, inspection, and deletion. |
31
+
32
+ ## Env vars
33
+
34
+ None.
35
+
36
+ ## Usage
37
+
38
+ ```ts
39
+ import { acquireFileLock } from "@poe-code/file-lock";
40
+
41
+ const release = await acquireFileLock("/repo/workflow.md");
42
+
43
+ try {
44
+ // Do work while holding the lock.
45
+ } finally {
46
+ await release();
47
+ }
48
+ ```
49
+
50
+ ## Notes
51
+
52
+ `@poe-code/agent-harness-tools` re-exports `acquireFileLock` as `lockWorkflow`.
@@ -0,0 +1 @@
1
+ export { acquireFileLock, LockTimeoutError, type FileLockFs, type FileLockOptions, type ReleaseLock } from "./lock.js";
@@ -0,0 +1 @@
1
+ export { acquireFileLock, LockTimeoutError } from "./lock.js";
@@ -0,0 +1,27 @@
1
+ export interface FileLockFs {
2
+ open(path: string, flags: string): Promise<{
3
+ close(): Promise<void>;
4
+ writeFile(data: string, options?: BufferEncoding | {
5
+ encoding?: BufferEncoding;
6
+ }): Promise<void>;
7
+ }>;
8
+ stat(path: string): Promise<{
9
+ mtimeMs: number;
10
+ }>;
11
+ readFile?(path: string, encoding: BufferEncoding): Promise<string>;
12
+ unlink(path: string): Promise<void>;
13
+ }
14
+ export interface FileLockOptions {
15
+ staleMs?: number;
16
+ retries?: number;
17
+ minTimeout?: number;
18
+ maxTimeout?: number;
19
+ fs?: FileLockFs;
20
+ isPidRunning?: (pid: number) => boolean;
21
+ signal?: AbortSignal;
22
+ }
23
+ export type ReleaseLock = () => Promise<void>;
24
+ export declare class LockTimeoutError extends Error {
25
+ constructor(message: string);
26
+ }
27
+ export declare function acquireFileLock(filePath: string, options?: FileLockOptions): Promise<ReleaseLock>;