chamba 0.3.1 → 0.5.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 (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -44
  3. package/bin/chamba.js +212 -0
  4. package/dist/commands/advanced.js +278 -0
  5. package/dist/commands/dev.js +612 -0
  6. package/dist/commands/doctor.js +29 -0
  7. package/dist/commands/menu.js +80 -0
  8. package/dist/commands/onboard.js +229 -0
  9. package/dist/commands/settings.js +349 -0
  10. package/dist/lib/agent-context.js +177 -0
  11. package/dist/lib/browser.js +40 -0
  12. package/dist/lib/chamba-yaml.js +191 -0
  13. package/dist/lib/constants.js +125 -0
  14. package/dist/lib/dockerfile-builder.js +267 -0
  15. package/dist/lib/env.js +78 -0
  16. package/dist/lib/global-config.js +66 -0
  17. package/dist/lib/pnpm-store.js +19 -0
  18. package/dist/lib/ports.js +210 -0
  19. package/dist/lib/safe-rm.js +26 -0
  20. package/dist/lib/sessions.js +34 -0
  21. package/dist/lib/shadows.js +174 -0
  22. package/dist/lib/webterm.js +305 -0
  23. package/dist/lib/workspace-identity.js +260 -0
  24. package/package.json +61 -23
  25. package/schema/chamba.schema.json +65 -0
  26. package/templates/.dockerignore +3 -0
  27. package/templates/Dockerfile +173 -0
  28. package/templates/claude-statusline.sh +120 -0
  29. package/templates/context/baseline.md +13 -0
  30. package/templates/context/context-usage.md +1 -0
  31. package/templates/context/git-mode-local.md +1 -0
  32. package/templates/context/git-mode-strict.md +1 -0
  33. package/templates/context/git-mode-unrestricted.md +1 -0
  34. package/templates/context/git-unavailable.md +1 -0
  35. package/templates/context/shadow-paths.md +3 -0
  36. package/templates/context-usage.sh +249 -0
  37. package/templates/git-readonly-wrapper.mjs +309 -0
  38. package/templates/npmrc +2 -0
  39. package/templates/pnpm-config.yaml +9 -0
  40. package/templates/runtime-constants.mjs +18 -0
  41. package/templates/skills/chamba-statusline/SKILL.md +79 -0
  42. package/templates/skills/context-usage/SKILL.md +53 -0
  43. package/templates/skills/web-pane/SKILL.md +62 -0
  44. package/templates/startup-git-mode.mjs +145 -0
  45. package/templates/startup.mjs +333 -0
  46. package/templates/webpane.sh +126 -0
  47. package/templates/webterm/README.md +160 -0
  48. package/templates/webterm/artifacts.js +583 -0
  49. package/templates/webterm/config.js +270 -0
  50. package/templates/webterm/context/claude.md +14 -0
  51. package/templates/webterm/conversation.js +253 -0
  52. package/templates/webterm/package-lock.json +884 -0
  53. package/templates/webterm/package.json +17 -0
  54. package/templates/webterm/pane.js +156 -0
  55. package/templates/webterm/proc.js +89 -0
  56. package/templates/webterm/public/app/alerts.js +472 -0
  57. package/templates/webterm/public/app/cards.js +123 -0
  58. package/templates/webterm/public/app/clipboard.js +229 -0
  59. package/templates/webterm/public/app/composer.js +226 -0
  60. package/templates/webterm/public/app/connection.js +342 -0
  61. package/templates/webterm/public/app/dictation.js +98 -0
  62. package/templates/webterm/public/app/dom.js +37 -0
  63. package/templates/webterm/public/app/drafts.js +244 -0
  64. package/templates/webterm/public/app/frames.js +166 -0
  65. package/templates/webterm/public/app/main.js +82 -0
  66. package/templates/webterm/public/app/new-session.js +188 -0
  67. package/templates/webterm/public/app/note.js +24 -0
  68. package/templates/webterm/public/app/pane-frame.js +166 -0
  69. package/templates/webterm/public/app/pane.js +353 -0
  70. package/templates/webterm/public/app/state.js +51 -0
  71. package/templates/webterm/public/app/status-strip.js +170 -0
  72. package/templates/webterm/public/app/tabs.js +475 -0
  73. package/templates/webterm/public/app/terminal.js +102 -0
  74. package/templates/webterm/public/app/theme.js +46 -0
  75. package/templates/webterm/public/favicon.svg +21 -0
  76. package/templates/webterm/public/index.html +105 -0
  77. package/templates/webterm/public/styles.css +1193 -0
  78. package/templates/webterm/resume.js +145 -0
  79. package/templates/webterm/server.js +1181 -0
  80. package/templates/webterm/sessions.js +515 -0
  81. package/templates/webterm/snapshot.js +135 -0
  82. package/templates/webterm.sh +164 -0
  83. package/dist/cli.js +0 -1582
  84. package/dist/server.js +0 -1831
  85. package/inject/annotate.js +0 -18
  86. package/skill/README.md +0 -12
  87. package/skill/SKILL.md +0 -93
  88. package/web/assets/highlighted-body-OFNGDK62-Bn4Eu7CG.js +0 -1
  89. package/web/assets/index-B9DI4F1Z.js +0 -202
  90. package/web/assets/index-DK_n6CTo.css +0 -2
  91. package/web/assets/mermaid-GHXKKRXX-CEMduc-U.js +0 -1
  92. package/web/index.html +0 -28
@@ -0,0 +1,145 @@
1
+ // resume.js - The command that reopens the conversation this workspace was last having.
2
+ //
3
+ // A container you come back to should open where you left off, so the first session of a container start
4
+ // continues the newest conversation instead of beginning one. Working out which conversation that is means
5
+ // reading the store the agent keeps it in, and this is the only place that can honestly do it: the stores
6
+ // are the container's own paths, and the CLIs that write them are installed here and nowhere else.
7
+ //
8
+ // Deliberately next to conversation.js, and deliberately not the same question. That file asks which
9
+ // conversation a live session is having, and answers it from the process tree and the session's start time.
10
+ // This one asks whether there is anything here worth reopening at all, before any session exists, so the
11
+ // directory is the only thing it has to go on.
12
+ //
13
+ // Null is the half that matters. Every one of these CLIs treats "resume, but there is nothing there" as an
14
+ // error and exits, so a resume command with nothing behind it opens the browser onto a session that died on
15
+ // arrival. Null means the session starts the agent fresh, which is the right answer for a workspace with no
16
+ // history.
17
+
18
+ import { readFileSync } from "node:fs";
19
+ import { basename, dirname } from "node:path";
20
+ import { CLAUDE_TRANSCRIPT, CODEX_ROLLOUT, filesNewestFirst } from "./conversation.js";
21
+
22
+ // --- Claude ------------------------------------------------------------------------------------------------------------------------------
23
+
24
+ /**
25
+ * Claude Code files its transcripts per directory, in a dir named after the directory it ran in with every
26
+ * character that is not a letter or a digit turned into "-": /workspace -> -workspace, /workspace/apps/api ->
27
+ * -workspace-apps-api. So a session in a sub-directory reads and writes a different dir from one at the root,
28
+ * and a resume command built from the wrong one names an id that claude cannot find.
29
+ * Only the fallback below relies on this naming; the main path reads what a transcript says about itself.
30
+ */
31
+ export function claudeProjectKey(containerPath) {
32
+ return containerPath.replace(/[^a-zA-Z0-9]/g, "-");
33
+ }
34
+
35
+ /**
36
+ * The most recent claude conversation that can be reopened in `cwd`: the newest transcript with a real
37
+ * (non-sidechain) user message that ran in that directory. Returns null when nothing qualifies.
38
+ *
39
+ * The directory is matched on what the transcript records, not on which dir it sits in, so however claude
40
+ * names those dirs we never hand back a conversation from somewhere else - resuming it would either fail or
41
+ * drop the user into another directory's history. Transcripts old enough to record no directory at all are
42
+ * the fallback, and for those the dir name is the only evidence there is.
43
+ */
44
+ function claudeResumeId(cwd, stores) {
45
+ const transcripts = filesNewestFirst(stores.claudeProjects, CLAUDE_TRANSCRIPT);
46
+ const byRecordedDir = firstResumable(transcripts, cwd);
47
+ if (byRecordedDir !== null) return byRecordedDir;
48
+ const key = claudeProjectKey(cwd);
49
+ return firstResumable(
50
+ transcripts.filter((path) => basename(dirname(path)) === key),
51
+ null,
52
+ );
53
+ }
54
+
55
+ // The first of these transcripts that can be reopened, in the order given. `requiredCwd` of null asks only
56
+ // that the conversation has messages; a directory asks that it recorded that one too. Transcripts are read
57
+ // in order and the walk stops at the first hit, so the usual case reads a single file.
58
+ function firstResumable(transcriptPaths, requiredCwd) {
59
+ for (const path of transcriptPaths) {
60
+ const facts = transcriptFacts(path);
61
+ if (!facts.resumable) continue;
62
+ if (requiredCwd !== null && facts.cwd !== requiredCwd) continue;
63
+ return basename(path).slice(0, -".jsonl".length);
64
+ }
65
+ return null;
66
+ }
67
+
68
+ /**
69
+ * What a transcript says about itself, in one pass: whether it holds a real user message, and the directory
70
+ * that message was typed in. Sessions that only recorded UI state (mode changes, slash commands) have
71
+ * nothing to resume; sidechain records belong to subagents. isSidechain is a field we do not own, so only an
72
+ * explicit true excludes a record: a record that stopped carrying the field still counts as resumable, which
73
+ * fails on the safe side. `cwd` is null for transcripts written before claude recorded it.
74
+ *
75
+ * Read whole rather than by the head, unlike conversation.js: the first user message can sit behind any
76
+ * amount of session setup, and answering "resumable" wrongly costs a dead session rather than a retry. The
77
+ * walk stops at the first hit, and the caller stops at the first resumable file, so the usual cost is the
78
+ * first few kilobytes of one transcript.
79
+ */
80
+ function transcriptFacts(transcriptPath) {
81
+ let content;
82
+ try {
83
+ content = readFileSync(transcriptPath, "utf8");
84
+ } catch {
85
+ return { resumable: false, cwd: null };
86
+ }
87
+ // Walked by index rather than split("\n") so a multi-MB transcript is not copied into an array of lines.
88
+ let start = 0;
89
+ while (start <= content.length) {
90
+ const end = content.indexOf("\n", start);
91
+ const line = content.slice(start, end === -1 ? content.length : end);
92
+ start = (end === -1 ? content.length : end) + 1;
93
+ // Cheap pre-filter so multi-MB transcripts are not JSON.parsed line by line.
94
+ if (!line.includes('"type":"user"')) continue;
95
+ try {
96
+ const record = JSON.parse(line);
97
+ if (record.type === "user" && record.isSidechain !== true) {
98
+ return { resumable: true, cwd: typeof record.cwd === "string" ? record.cwd : null };
99
+ }
100
+ } catch {
101
+ // Skip malformed lines.
102
+ }
103
+ }
104
+ return { resumable: false, cwd: null };
105
+ }
106
+
107
+ // --- Codex and opencode ------------------------------------------------------------------------------------------------------------------
108
+
109
+ // What reopens a stored conversation, per agent, and where to look to know there is one. claude is not here:
110
+ // it is resumed by id above, because `claude --continue` takes the newest transcript by mtime and silently
111
+ // starts FRESH when that transcript holds no real messages, which makes auto-resume fail quietly whenever
112
+ // such a session happens to be the newest.
113
+ //
114
+ // These two are asked a shallower question than claude - does the store hold a conversation file at all -
115
+ // and their own flag picks which one. Reading each store's format to find out whether the newest
116
+ // conversation is really resumable would tie chamba to two more internal layouts for no gain.
117
+ export const CONTINUE = {
118
+ opencode: { argv: ["opencode", "--continue"], store: "opencodeSessions", pattern: /\.json$/ },
119
+ codex: { argv: ["codex", "resume", "--last"], store: "codexSessions", pattern: CODEX_ROLLOUT },
120
+ };
121
+
122
+ // --- The one entry point -----------------------------------------------------------------------------------------------------------------
123
+
124
+ /**
125
+ * The argv that reopens this workspace's most recent conversation for `agent` in `cwd`, or null when there
126
+ * is nothing to reopen. `stores` is config.js's AGENT_STORES, passed in so this can be exercised against a
127
+ * fixture the way conversation.js is.
128
+ *
129
+ * An agent nobody wrote a reader for is a supported answer, not an error: it starts fresh.
130
+ */
131
+ export function resumeArgvFor(agent, cwd, stores) {
132
+ try {
133
+ if (agent === "claude") {
134
+ const id = claudeResumeId(cwd, stores);
135
+ return id === null ? null : ["claude", "--resume", id];
136
+ }
137
+ const known = CONTINUE[agent];
138
+ if (!known) return null;
139
+ return filesNewestFirst(stores[known.store], known.pattern).length > 0 ? [...known.argv] : null;
140
+ } catch {
141
+ // A store that changed shape under us must never take the session down with it - starting fresh is
142
+ // a worse session than the user asked for, but it is a session.
143
+ return null;
144
+ }
145
+ }