@amenophis1er/foreman 0.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 (65) hide show
  1. package/DESIGN.md +408 -0
  2. package/LICENSE +15 -0
  3. package/README.md +133 -0
  4. package/bin/foreman.mjs +58 -0
  5. package/package.json +68 -0
  6. package/scripts/prepare.mjs +48 -0
  7. package/skills/director/SKILL.md +65 -0
  8. package/src/anthropic-models.ts +54 -0
  9. package/src/ask.test.ts +88 -0
  10. package/src/ask.ts +95 -0
  11. package/src/attachments.test.ts +33 -0
  12. package/src/attachments.ts +60 -0
  13. package/src/cli.test.ts +27 -0
  14. package/src/cli.ts +297 -0
  15. package/src/codex.test.ts +328 -0
  16. package/src/codex.ts +196 -0
  17. package/src/cost-basis.test.ts +76 -0
  18. package/src/deck.test.ts +402 -0
  19. package/src/deck.ts +892 -0
  20. package/src/fork.test.ts +31 -0
  21. package/src/gateway/ledger.cjs +326 -0
  22. package/src/gateway/ledger.test.ts +255 -0
  23. package/src/gateway/llm-gateway.cjs +1411 -0
  24. package/src/gateway/llm-gateway.test.ts +478 -0
  25. package/src/gateway.test.ts +226 -0
  26. package/src/gateway.ts +309 -0
  27. package/src/instance.ts +124 -0
  28. package/src/models.test.ts +147 -0
  29. package/src/models.ts +158 -0
  30. package/src/notify/commands.test.ts +28 -0
  31. package/src/notify/commands.ts +73 -0
  32. package/src/notify/telegram.ts +259 -0
  33. package/src/notify.test.ts +343 -0
  34. package/src/notify.ts +495 -0
  35. package/src/ollama.test.ts +49 -0
  36. package/src/ollama.ts +49 -0
  37. package/src/openai-prices.test.ts +58 -0
  38. package/src/openai-prices.ts +106 -0
  39. package/src/orchestrator.test.ts +1147 -0
  40. package/src/orchestrator.ts +2325 -0
  41. package/src/planner.test.ts +60 -0
  42. package/src/planner.ts +505 -0
  43. package/src/policy.test.ts +411 -0
  44. package/src/policy.ts +599 -0
  45. package/src/preflight.ts +348 -0
  46. package/src/prices.test.ts +69 -0
  47. package/src/prices.ts +90 -0
  48. package/src/provider.test.ts +366 -0
  49. package/src/provider.ts +502 -0
  50. package/src/secrets.test.ts +143 -0
  51. package/src/secrets.ts +66 -0
  52. package/src/server.ts +1992 -0
  53. package/src/services.test.ts +53 -0
  54. package/src/services.ts +102 -0
  55. package/src/sse-events.test.ts +83 -0
  56. package/src/store.test.ts +119 -0
  57. package/src/store.ts +346 -0
  58. package/src/tailscale.test.ts +32 -0
  59. package/src/tailscale.ts +79 -0
  60. package/src/title.ts +138 -0
  61. package/src/types.ts +442 -0
  62. package/ui/dist/assets/index-LAj0Dy9p.css +1 -0
  63. package/ui/dist/assets/index-lcBy-uRZ.js +65 -0
  64. package/ui/dist/favicon.svg +8 -0
  65. package/ui/dist/index.html +14 -0
package/src/secrets.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Provider keys, kept on disk.
3
+ *
4
+ * This is the first thing Foreman is custodian of. Everything else it
5
+ * authenticates with belongs to somebody else's CLI — `~/.claude`,
6
+ * `~/.codex` — and the standing rule in docs/provider-model.md §5 is to read
7
+ * what a first-party tool already wrote rather than mint anything. That rule
8
+ * survives: it is about *minting*, not storing, and it still decides the
9
+ * default. Lean on a CLI wherever one exists; store a key only where there is
10
+ * none to lean on — OpenAI, OpenRouter, a self-hosted endpoint.
11
+ *
12
+ * Rules this module exists to keep:
13
+ *
14
+ * - A key lives in the provider's own Foreman-owned config dir, mode 0600,
15
+ * and nowhere else. Never in `projects.json`, which is world-readable
16
+ * ordinary config people copy between machines and paste into issues.
17
+ * - No route ever returns one. The API answers "is there a key" and nothing
18
+ * more, so a compromised browser session cannot read out what it can only
19
+ * replace.
20
+ * - Nothing here logs, throws with, or embeds a key in an error message.
21
+ */
22
+ import { chmod, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
23
+ import path from 'node:path';
24
+ import crypto from 'node:crypto';
25
+ import { ownedConfigDir } from './provider.js';
26
+
27
+ /** Mode 0600: readable by the user who runs Foreman, nobody else. */
28
+ const KEY_MODE = 0o600;
29
+
30
+ function keyFile(root: string, providerId: string): string {
31
+ return path.join(ownedConfigDir(root, providerId), 'key');
32
+ }
33
+
34
+ /**
35
+ * Stores a provider's key, replacing any previous one.
36
+ *
37
+ * Written tmp-then-rename like every other file Foreman owns, but with the
38
+ * mode set *before* the rename: creating the real file world-readable and
39
+ * tightening it afterwards leaves a window where it is not.
40
+ */
41
+ export async function putSecret(root: string, providerId: string, key: string): Promise<void> {
42
+ const file = keyFile(root, providerId);
43
+ const dir = path.dirname(file);
44
+ await mkdir(dir, { recursive: true });
45
+ const tmp = path.join(dir, `.key.${crypto.randomBytes(4).toString('hex')}.tmp`);
46
+ await writeFile(tmp, key, { mode: KEY_MODE });
47
+ await chmod(tmp, KEY_MODE);
48
+ await rename(tmp, file);
49
+ }
50
+
51
+ /** The stored key, or null. The only function that returns one. */
52
+ export async function getSecret(root: string, providerId: string): Promise<string | null> {
53
+ const raw = await readFile(keyFile(root, providerId), 'utf8').catch(() => null);
54
+ const key = raw?.trim();
55
+ return key ? key : null;
56
+ }
57
+
58
+ /** Whether a key is stored — what the API is allowed to say. */
59
+ export async function hasSecret(root: string, providerId: string): Promise<boolean> {
60
+ return stat(keyFile(root, providerId)).then((st) => st.isFile(), () => false);
61
+ }
62
+
63
+ /** Forgets a provider's key. Absent is success: the caller wanted it gone. */
64
+ export async function deleteSecret(root: string, providerId: string): Promise<void> {
65
+ await rm(keyFile(root, providerId), { force: true });
66
+ }