loopat 0.1.46 → 0.1.48
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.
- package/package.json +3 -2
- package/server/src/loops.ts +8 -2
- package/server/src/podman.ts +40 -12
- package/server/src/session.ts +1 -0
- package/server/src/term.ts +1 -0
- package/web/dist/assets/{CodeEditor-DtHZtsPs.js → CodeEditor-JV36Z3V5.js} +8 -8
- package/web/dist/assets/Editor-CeZLgps-.js +1 -0
- package/web/dist/assets/Markdown-CnZpm-Uk.js +5 -0
- package/web/dist/assets/{MilkdownEditor-BsIWrDWi.js → MilkdownEditor-t_CG4MJj.js} +9 -9
- package/web/dist/assets/{Terminal-trnCVajY.js → Terminal-Vi3Ufhgi.js} +2 -2
- package/web/dist/assets/index-C7az8Kc1.js +145 -0
- package/web/dist/assets/{jsx-runtime-Bt-cYkS5.js → jsx-runtime-DAYmCNe8.js} +1 -1
- package/web/dist/assets/lib-B8L80SIn.js +18 -0
- package/web/dist/index.html +3 -2
- package/web/dist/assets/Editor-DJDW81ID.js +0 -1
- package/web/dist/assets/Markdown-C4to5tAn.js +0 -5
- package/web/dist/assets/index-AEGKdly-.js +0 -162
- /package/web/dist/assets/{w3c-keyname-BOAvb0qz.js → w3c-keyname-DXh_HxYD.js} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"description": "Self-hosted AI workspace built around context management — works solo, scales to teams",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/simpx/loopat",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"prepublishOnly": "bun install && bun run build:web",
|
|
38
38
|
"postinstall": "node scripts/install-sandbox-claude.mjs",
|
|
39
39
|
"test:e2e": "playwright test",
|
|
40
|
-
"test:e2e:ui": "playwright test --ui"
|
|
40
|
+
"test:e2e:ui": "playwright test --ui",
|
|
41
|
+
"dogfood": "playwright test --config dogfood/playwright.config.ts"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"@anthropic-ai/claude-agent-sdk": "^0.3.150",
|
package/server/src/loops.ts
CHANGED
|
@@ -1996,8 +1996,14 @@ async function _ensureContextWorktree(repo: string, path: string, branchName: st
|
|
|
1996
1996
|
const start = await remoteStartPoint(repo)
|
|
1997
1997
|
// -B (not -b): reset the branch if it lingers from a removed worktree, so a
|
|
1998
1998
|
// rebuild always re-opens cleanly from the start point.
|
|
1999
|
-
|
|
2000
|
-
|
|
1999
|
+
// No start point → `worktree add -B <branch> <path>` tries to seed the new
|
|
2000
|
+
// branch from the main repo's HEAD. For an EMPTY context remote (e.g. a notes
|
|
2001
|
+
// repo with no commits) the clone's HEAD is an unborn ref (`ref: main`); once
|
|
2002
|
+
// the repo later gains an unrelated branch (master, pushed by another loop)
|
|
2003
|
+
// git no longer infers `--orphan` and dies with `fatal: invalid reference:
|
|
2004
|
+
// HEAD`. Pass `--orphan` ourselves so a startless worktree never depends on a
|
|
2005
|
+
// resolvable HEAD — it opens an empty branch the loop can commit onto.
|
|
2006
|
+
const tail = start ? ["-B", branchName, path, start] : ["--orphan", "-B", branchName, path]
|
|
2001
2007
|
|
|
2002
2008
|
// git-crypt + worktree: the git-crypt key lives in the MAIN repo's
|
|
2003
2009
|
// `.git/git-crypt`, but a worktree's gitdir is separate and has no key — so a
|
package/server/src/podman.ts
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
personalKnowledgeDir,
|
|
53
53
|
personalNotesDir,
|
|
54
54
|
personalReposDir,
|
|
55
|
+
personalRepoCacheDir,
|
|
55
56
|
LOOPAT_INSTALL_DIR,
|
|
56
57
|
loopHomeUpper,
|
|
57
58
|
workspaceHomeSkelDir,
|
|
@@ -85,14 +86,20 @@ export const V_CONTEXT_CHAT = "/loopat/context/chat"
|
|
|
85
86
|
export const V_HOST_EXEC_DIR = "/loopat/host-exec"
|
|
86
87
|
export const V_HOST_EXEC_SOCK = "/loopat/host-exec/host-exec.sock"
|
|
87
88
|
|
|
88
|
-
// $HOME inside the container.
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// /loopat
|
|
94
|
-
//
|
|
95
|
-
|
|
89
|
+
// $HOME inside the container. MUST equal the sandbox user's /etc/passwd home,
|
|
90
|
+
// otherwise ssh/git resolve `~` (e.g. ~/.ssh) via getpwuid (= passwd home), NOT
|
|
91
|
+
// $HOME — so a vault mounted at $HOME/.ssh is invisible to ssh and every
|
|
92
|
+
// sandbox-side `git push`/clone fails "Host key verification failed" / can't
|
|
93
|
+
// find the key. The image's `loopat` user (uid 2000) has passwd home
|
|
94
|
+
// /home/loopat, so we use exactly that.
|
|
95
|
+
//
|
|
96
|
+
// Still NOT the host's homedir: binding host $HOME at its real path makes podman
|
|
97
|
+
// auto-create nested-bind parent dirs owned by a subuid the host can't delete.
|
|
98
|
+
// /home/loopat is a CONTAINER-internal path — host-absolute binds sit outside
|
|
99
|
+
// it, and it vanishes with the container, so there's no host residue. (Per-user
|
|
100
|
+
// distinction is unnecessary: each loop has its own isolated container + home
|
|
101
|
+
// overlay; the home path inside need not encode the user.)
|
|
102
|
+
export const V_HOME = (_user: string) => `/home/loopat`
|
|
96
103
|
|
|
97
104
|
// Label keys for podman inspect.
|
|
98
105
|
const LABEL_LOOP = "loopat.loop-id"
|
|
@@ -106,7 +113,12 @@ const LABEL_CONFIG_HASH = "loopat.config-hash"
|
|
|
106
113
|
// by the loopat.workspace label, not this name — the name only prevents tag
|
|
107
114
|
// collisions. Same-Containerfile builds still share overlay layers, so the
|
|
108
115
|
// per-workspace tags don't multiply disk usage.
|
|
109
|
-
|
|
116
|
+
// Image tag is content-addressed (no workspace prefix) so the same image is
|
|
117
|
+
// reused across workspaces/LOOPAT_HOMEs instead of rebuilt per workspace. The
|
|
118
|
+
// trade-off (deliberate): deleting a workspace no longer prunes its images —
|
|
119
|
+
// we'd rather leave a residual image than rebuild on every fresh workspace.
|
|
120
|
+
// Containers + their LABEL_WORKSPACE stay workspace-scoped (runtime isolation).
|
|
121
|
+
export const SANDBOX_IMAGE = process.env.LOOPAT_SANDBOX_IMAGE || `loopat-sandbox:latest`
|
|
110
122
|
// Prebuilt multi-arch base image published to GHCR by CI, tagged by the
|
|
111
123
|
// Containerfile content hash. ensureSandboxImage pulls this instead of building
|
|
112
124
|
// locally — a pull is faster and far more reliable than apt-installing ~150
|
|
@@ -128,6 +140,11 @@ export type ContainerOptions = {
|
|
|
128
140
|
vaultName?: string
|
|
129
141
|
knowledgeRw?: boolean
|
|
130
142
|
mountAllLoops?: boolean
|
|
143
|
+
/** Source roster repo for this loop's workdir (meta.repo). The workdir is a
|
|
144
|
+
* `git worktree add` off this repo's bare mirror (repo-cache/<repo>), so its
|
|
145
|
+
* .git gitdir points into that mirror. When set, the mirror is bind-mounted
|
|
146
|
+
* at its host path (src=dst, rw) so the worktree resolves inside the sandbox. */
|
|
147
|
+
repo?: string
|
|
131
148
|
/** Extra env vars to pre-bake into the container at create time. */
|
|
132
149
|
extraEnv?: Record<string, string>
|
|
133
150
|
/** Image to create the container from. Defaults to SANDBOX_IMAGE.
|
|
@@ -190,7 +207,7 @@ export type VolumeMount = {
|
|
|
190
207
|
*/
|
|
191
208
|
export async function buildVolumeMounts(opts: ContainerOptions): Promise<VolumeMount[]> {
|
|
192
209
|
const hostHome = homedir()
|
|
193
|
-
const { loopId, createdBy, vaultName, knowledgeRw, mountAllLoops } = opts
|
|
210
|
+
const { loopId, createdBy, vaultName, knowledgeRw, mountAllLoops, repo } = opts
|
|
194
211
|
const virtualHome = V_HOME(createdBy)
|
|
195
212
|
const mounts: VolumeMount[] = []
|
|
196
213
|
|
|
@@ -204,6 +221,17 @@ export async function buildVolumeMounts(opts: ContainerOptions): Promise<VolumeM
|
|
|
204
221
|
|
|
205
222
|
// Virtual mount points for AI / user:
|
|
206
223
|
mounts.push({ src: loopWorkdir(loopId), dst: V_LOOP_WORKDIR(loopId) })
|
|
224
|
+
|
|
225
|
+
// Workdir built from a roster repo is a `git worktree add` off the repo's
|
|
226
|
+
// bare mirror (repo-cache/<repo>); the workdir's .git is a gitdir pointer INTO
|
|
227
|
+
// that mirror. Bind the mirror at its host path (src=dst) so the worktree's
|
|
228
|
+
// objects/refs/worktree-metadata resolve inside the sandbox — otherwise
|
|
229
|
+
// `git status` in the workdir is "fatal: not a git repository". rw because git
|
|
230
|
+
// writes the worktree's index/HEAD/logs under <mirror>/worktrees/<wt>/.
|
|
231
|
+
if (repo) {
|
|
232
|
+
const cache = personalRepoCacheDir(createdBy, repo)
|
|
233
|
+
mounts.push({ src: cache, dst: cache })
|
|
234
|
+
}
|
|
207
235
|
mounts.push({ src: loopClaudeDir(loopId), dst: V_LOOP_CLAUDE(loopId) })
|
|
208
236
|
mounts.push({
|
|
209
237
|
src: loopContextKnowledge(loopId),
|
|
@@ -582,7 +610,7 @@ export async function ensureSandboxImage(opts?: { onProgress?: (msg: string) =>
|
|
|
582
610
|
|
|
583
611
|
// Hash the Containerfile so the base image auto-rebuilds when it changes.
|
|
584
612
|
const hash = await baseContainerfileHash()
|
|
585
|
-
const hashTag = `loopat-sandbox-${
|
|
613
|
+
const hashTag = `loopat-sandbox-${hash}:latest`
|
|
586
614
|
|
|
587
615
|
const present = await runPodman(["image", "exists", hashTag], { allowFail: true })
|
|
588
616
|
if (present.code === 0) {
|
|
@@ -704,7 +732,7 @@ export async function ensureLoopImage(loopId: string, opts?: { onProgress?: (msg
|
|
|
704
732
|
// after the nested-podman base change shipped).
|
|
705
733
|
const baseHash = await baseContainerfileHash()
|
|
706
734
|
const hash = createHash("sha256").update(`base:${baseHash}\n`).update(content).digest("hex").slice(0, 16)
|
|
707
|
-
const tag = `loopat-sandbox-${
|
|
735
|
+
const tag = `loopat-sandbox-${hash}:latest`
|
|
708
736
|
|
|
709
737
|
const existing = _loopImageInFlight.get(tag)
|
|
710
738
|
if (existing) return existing
|
package/server/src/session.ts
CHANGED
package/server/src/term.ts
CHANGED
|
@@ -67,6 +67,7 @@ async function getOrSpawn(loopId: string, initCols = 80, initRows = 24): Promise
|
|
|
67
67
|
vaultName: meta.config?.vault,
|
|
68
68
|
knowledgeRw: meta.config?.knowledge_rw,
|
|
69
69
|
mountAllLoops: meta.config?.mount_all_loops,
|
|
70
|
+
repo: meta.repo,
|
|
70
71
|
extraEnv: personalCfg.vaultEnvs,
|
|
71
72
|
ephemeralPorts: loopEphemeralPorts(meta),
|
|
72
73
|
}, {
|