loopat 0.1.20 → 0.1.22
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 -1
- package/scripts/install-sandbox-claude.mjs +49 -0
- package/server/src/bootstrap.ts +11 -6
- package/server/src/claude-binary.ts +53 -2
- package/server/src/index.ts +8 -0
- package/server/src/podman.ts +2 -2
- package/server/src/session.ts +5 -2
- package/web/dist/assets/{CodeEditor-DL99b11Y.js → CodeEditor-CCmzcfl4.js} +2 -2
- package/web/dist/assets/Editor-DjEYNpQa.js +1 -0
- package/web/dist/assets/{Markdown-DS_QLUsd.js → Markdown-DL99Gl_M.js} +2 -2
- package/web/dist/assets/{MilkdownEditor-DDhGpwvK.js → MilkdownEditor-CDYnLn81.js} +7 -7
- package/web/dist/assets/{Terminal-dhf2INUI.js → Terminal-DTMXO4Pg.js} +2 -2
- package/web/dist/assets/{index-Po2JG3Ae.js → index-Bpe3D3u2.js} +81 -81
- package/web/dist/assets/jsx-runtime-29NaZBia.js +1 -0
- package/web/dist/index.html +2 -1
- package/web/dist/assets/Editor-DbkUcYr-.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "loopat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
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",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"bin/",
|
|
17
|
+
"scripts/install-sandbox-claude.mjs",
|
|
17
18
|
"server/src/",
|
|
18
19
|
"!server/src/serve-rs",
|
|
19
20
|
"!server/src/port-proxy-rs",
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
"build": "bun install && (cd web && bun run build)",
|
|
35
36
|
"build:web": "cd web && bunx tsc -b && bunx vite build",
|
|
36
37
|
"prepublishOnly": "bun install && bun run build:web",
|
|
38
|
+
"postinstall": "node scripts/install-sandbox-claude.mjs",
|
|
37
39
|
"test:e2e": "playwright test",
|
|
38
40
|
"test:e2e:ui": "playwright test --ui"
|
|
39
41
|
},
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postinstall: make sure a LINUX claude binary is available for the sandbox.
|
|
4
|
+
*
|
|
5
|
+
* loopat runs the AI inside a linux podman sandbox, so it needs the
|
|
6
|
+
* linux-<arch> claude binary. npm only installs the claude-agent-sdk platform
|
|
7
|
+
* binary matching the HOST (os/cpu filtered optionalDependencies) — so on a
|
|
8
|
+
* linux host we already have it (no-op here), but on macOS/Windows npm installs
|
|
9
|
+
* the darwin/win binary and the sandbox would hit "Exec format error".
|
|
10
|
+
*
|
|
11
|
+
* On a non-linux host we fetch the linux-<arch> binary into
|
|
12
|
+
* <loopat>/sandbox-claude (pinned to the SDK version we depend on) using npm's
|
|
13
|
+
* --os/--cpu override. Best-effort: a failure only means sandbox AI won't run
|
|
14
|
+
* on this host until fixed; the install itself still succeeds.
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync } from "node:child_process"
|
|
17
|
+
import { existsSync, mkdirSync } from "node:fs"
|
|
18
|
+
import { dirname, join, resolve } from "node:path"
|
|
19
|
+
import { fileURLToPath } from "node:url"
|
|
20
|
+
import { createRequire } from "node:module"
|
|
21
|
+
|
|
22
|
+
if (process.platform === "linux") process.exit(0) // host claude IS the sandbox claude
|
|
23
|
+
|
|
24
|
+
const arch = process.arch // "arm64" | "x64"
|
|
25
|
+
const pkg = `@anthropic-ai/claude-agent-sdk-linux-${arch}`
|
|
26
|
+
const installDir = resolve(dirname(fileURLToPath(import.meta.url)), "..")
|
|
27
|
+
const dest = join(installDir, "sandbox-claude")
|
|
28
|
+
const binary = join(dest, "node_modules", pkg, "claude")
|
|
29
|
+
|
|
30
|
+
if (existsSync(binary)) process.exit(0) // already fetched
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const require = createRequire(import.meta.url)
|
|
34
|
+
let version = ""
|
|
35
|
+
try {
|
|
36
|
+
version = require("@anthropic-ai/claude-agent-sdk/package.json").version
|
|
37
|
+
} catch {}
|
|
38
|
+
const spec = version ? `${pkg}@${version}` : pkg
|
|
39
|
+
mkdirSync(dest, { recursive: true })
|
|
40
|
+
console.log(`[loopat] host is ${process.platform}/${arch}; fetching linux claude for the sandbox (${spec})…`)
|
|
41
|
+
// The platform binary declares os=linux, so `--os=linux --cpu` hits
|
|
42
|
+
// EBADPLATFORM on a darwin host — `--force` is what actually fetches it.
|
|
43
|
+
execFileSync("npm", ["install", "--prefix", dest, "--no-save", "--force", spec], { stdio: "inherit" })
|
|
44
|
+
if (existsSync(binary)) console.log(`[loopat] sandbox claude ready at ${binary}`)
|
|
45
|
+
else console.warn(`[loopat] sandbox claude install finished but ${binary} is missing`)
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.warn(`[loopat] could not fetch linux claude for the sandbox: ${e?.message ?? e}`)
|
|
48
|
+
console.warn(`[loopat] sandbox AI won't run on this host until fixed; everything else works.`)
|
|
49
|
+
}
|
package/server/src/bootstrap.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { existsSync } from "node:fs"
|
|
7
7
|
import { execFileSync } from "node:child_process"
|
|
8
8
|
import { join } from "node:path"
|
|
9
|
-
import {
|
|
9
|
+
import { resolveSandboxClaudeBinary } from "./claude-binary"
|
|
10
10
|
import { configPath, loadKnowledgeConfig, type WorkspaceConfig, type KnowledgeConfig } from "./config"
|
|
11
11
|
import {
|
|
12
12
|
WORKSPACE,
|
|
@@ -68,14 +68,19 @@ function checkPodman(): Check {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
function checkClaudeBinary(): Check {
|
|
71
|
+
// The AI runs in the linux sandbox, so what matters is the SANDBOX claude.
|
|
71
72
|
try {
|
|
72
|
-
const p =
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
const p = resolveSandboxClaudeBinary()
|
|
74
|
+
const tag = process.platform === "linux" ? "" : " [linux, for sandbox]"
|
|
75
|
+
return { ok: true, label: `claude binary${tag} (${p.split("/").slice(-3).join("/")})` }
|
|
76
|
+
} catch {
|
|
75
77
|
return {
|
|
76
78
|
ok: false,
|
|
77
|
-
label: "claude binary",
|
|
78
|
-
hint:
|
|
79
|
+
label: "claude binary (sandbox/linux)",
|
|
80
|
+
hint:
|
|
81
|
+
process.platform === "linux"
|
|
82
|
+
? "run `bun install` in the loopat repo root — SDK ships the binary as a platform-specific package"
|
|
83
|
+
: "the linux claude for the sandbox wasn't fetched (postinstall). Reinstall loopat, or run the `npm install --os=linux ...` command from the resolve error",
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { existsSync } from "node:fs"
|
|
2
|
-
import { execSync } from "node:child_process"
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs"
|
|
2
|
+
import { execSync, execFile } from "node:child_process"
|
|
3
|
+
import { promisify } from "node:util"
|
|
3
4
|
import { fileURLToPath } from "node:url"
|
|
4
5
|
import { dirname, resolve, join } from "node:path"
|
|
6
|
+
import { createRequire } from "node:module"
|
|
7
|
+
|
|
8
|
+
const execFileP = promisify(execFile)
|
|
5
9
|
|
|
6
10
|
function detectIsMusl(): boolean {
|
|
7
11
|
if (process.platform !== "linux") return false
|
|
@@ -66,3 +70,50 @@ export function resolveClaudeBinary(): string {
|
|
|
66
70
|
}
|
|
67
71
|
throw new Error(`claude binary not found; tried:\n${candidates.join("\n")}`)
|
|
68
72
|
}
|
|
73
|
+
|
|
74
|
+
/** Where the fetched linux claude lives on a non-linux host. */
|
|
75
|
+
function sandboxClaudeDir(): string {
|
|
76
|
+
const installDir = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..")
|
|
77
|
+
return join(installDir, "sandbox-claude")
|
|
78
|
+
}
|
|
79
|
+
function sandboxClaudeBinaryPath(): string {
|
|
80
|
+
return join(sandboxClaudeDir(), "node_modules", "@anthropic-ai", `claude-agent-sdk-linux-${process.arch}`, "claude")
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The claude binary the SANDBOX runs (the AI executes inside a linux podman
|
|
85
|
+
* container). On a linux host that's just the host claude. On a non-linux host
|
|
86
|
+
* npm only installed the host (e.g. darwin) binary, so we fetch the linux-<arch>
|
|
87
|
+
* one into <loopat>/sandbox-claude — bind THAT into the sandbox, not the host
|
|
88
|
+
* binary (otherwise: "Exec format error").
|
|
89
|
+
*/
|
|
90
|
+
export function resolveSandboxClaudeBinary(): string {
|
|
91
|
+
if (process.platform === "linux") return resolveClaudeBinary()
|
|
92
|
+
const candidate = sandboxClaudeBinaryPath()
|
|
93
|
+
if (existsSync(candidate)) return candidate
|
|
94
|
+
throw new Error(`sandbox (linux) claude not found at ${candidate}; run ensureSandboxClaudeBinary() (loopat fetches it on first boot)`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Make sure the sandbox's linux claude exists, fetching it if not. No-op on a
|
|
99
|
+
* linux host (host claude IS the sandbox claude). On a non-linux host this runs
|
|
100
|
+
* `npm install --force` (the platform binary has os=linux, so --os/--cpu hit
|
|
101
|
+
* EBADPLATFORM; --force is what gets it). Pinned to the SDK version. Best-effort
|
|
102
|
+
* and idempotent: once fetched, returns immediately. npx does NOT run package
|
|
103
|
+
* postinstall scripts, so this boot-time fetch is what actually covers
|
|
104
|
+
* `npx loopat`.
|
|
105
|
+
*/
|
|
106
|
+
export async function ensureSandboxClaudeBinary(onLog?: (m: string) => void): Promise<void> {
|
|
107
|
+
if (process.platform === "linux") return
|
|
108
|
+
if (existsSync(sandboxClaudeBinaryPath())) return
|
|
109
|
+
const arch = process.arch
|
|
110
|
+
let version = ""
|
|
111
|
+
try { version = createRequire(import.meta.url)("@anthropic-ai/claude-agent-sdk/package.json").version } catch {}
|
|
112
|
+
const spec = `@anthropic-ai/claude-agent-sdk-linux-${arch}${version ? `@${version}` : ""}`
|
|
113
|
+
const dest = sandboxClaudeDir()
|
|
114
|
+
mkdirSync(dest, { recursive: true })
|
|
115
|
+
onLog?.(`host is ${process.platform}/${arch}; fetching linux claude for the sandbox (${spec})…`)
|
|
116
|
+
await execFileP("npm", ["install", "--prefix", dest, "--no-save", "--force", spec], { timeout: 180_000 })
|
|
117
|
+
if (!existsSync(sandboxClaudeBinaryPath())) throw new Error(`fetch finished but ${sandboxClaudeBinaryPath()} missing`)
|
|
118
|
+
onLog?.(`sandbox claude ready`)
|
|
119
|
+
}
|
package/server/src/index.ts
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
import { loadConfig, loadPersonalConfig, savePersonalConfig, saveWorkspaceConfig, loadTokenUsage, getActiveProvider, readPersonalDiskRaw, savePersonalDisk, describeApiKeyRef, writeVaultEnv, deleteVaultEnv, loadKnowledgeConfig, saveKnowledgeConfig, type ProviderConfig, type ModelEntry } from "./config"
|
|
59
59
|
import { listBoards, createBoard, renameBoard, listKanbanColumns, addCard, toggleCard, deleteCard, moveCard, updateCardMeta, updateCardBlock, reorderCards, createColumn, deleteColumn, readKanbanConfig, saveColumnOrder, setColumnColor, renameColumn, assignDriverForCard, createLoopFromCard, linkLoopToCard, kanbanUserCtx } from "./kanban"
|
|
60
60
|
import { printBootstrapBanner } from "./bootstrap"
|
|
61
|
+
import { ensureSandboxClaudeBinary } from "./claude-binary"
|
|
61
62
|
import { serveHostExec, hostExecSocketPath } from "./host-exec"
|
|
62
63
|
import {
|
|
63
64
|
createUser,
|
|
@@ -3170,6 +3171,13 @@ console.log(`[loopat] server listening on http://${hostname}:${port}`)
|
|
|
3170
3171
|
}
|
|
3171
3172
|
}
|
|
3172
3173
|
|
|
3174
|
+
// On a non-linux host the sandbox needs a linux claude that npm/npx didn't
|
|
3175
|
+
// install (npx skips postinstall scripts) — fetch it in the background on first
|
|
3176
|
+
// boot (~18s, doesn't block the port; skipped once present / on a linux host).
|
|
3177
|
+
ensureSandboxClaudeBinary((m) => console.log(`[loopat] ${m}`)).catch((e) =>
|
|
3178
|
+
console.warn(`[loopat] sandbox claude fetch failed: ${e?.message ?? e}; sandbox AI won't run until fixed`),
|
|
3179
|
+
)
|
|
3180
|
+
|
|
3173
3181
|
await printBootstrapBanner(cfg)
|
|
3174
3182
|
const backfilled = await backfillAllMounts()
|
|
3175
3183
|
if (backfilled > 0) console.log(`[loopat] backfilled context mounts on ${backfilled} loop(s)`)
|
package/server/src/podman.ts
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
import { loadConfig } from "./config"
|
|
61
61
|
import { DEFAULT_VAULT, listVaultHomeMounts } from "./vaults"
|
|
62
62
|
import { hostExecDir, writeHostShims } from "./host-exec"
|
|
63
|
-
import {
|
|
63
|
+
import { resolveSandboxClaudeBinary } from "./claude-binary"
|
|
64
64
|
import { parse as tomlParse, stringify as tomlStringify } from "smol-toml"
|
|
65
65
|
|
|
66
66
|
const execFileP = promisify(execFile)
|
|
@@ -221,7 +221,7 @@ export async function buildVolumeMounts(opts: ContainerOptions): Promise<VolumeM
|
|
|
221
221
|
// sandbox exec's it by its host path, so bind that path in (ro) when it isn't
|
|
222
222
|
// already covered by the install-dir mount — otherwise the AI is code 127.
|
|
223
223
|
try {
|
|
224
|
-
const claudeDir = dirname(
|
|
224
|
+
const claudeDir = dirname(resolveSandboxClaudeBinary())
|
|
225
225
|
if (existsSync(claudeDir) && !claudeDir.startsWith(LOOPAT_INSTALL_DIR)) {
|
|
226
226
|
mounts.push({ src: claudeDir, dst: claudeDir, ro: true })
|
|
227
227
|
}
|
package/server/src/session.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { createWriteStream, mkdirSync, existsSync } from "node:fs"
|
|
|
5
5
|
import { randomUUID } from "node:crypto"
|
|
6
6
|
import { join } from "node:path"
|
|
7
7
|
import { loopClaudeDir, loopDir, loopHistoryPath, personalSkillsDir, workspaceTeamSkillsDir } from "./paths"
|
|
8
|
-
import {
|
|
8
|
+
import { resolveSandboxClaudeBinary } from "./claude-binary"
|
|
9
9
|
import { loadConfig, loadPersonalConfig, parseDefault, type ProviderConfig } from "./config"
|
|
10
10
|
import { buildLoopatAppend } from "./system-prompt"
|
|
11
11
|
import { composeLoopClaudeConfig, writeLoopSettings } from "./compose"
|
|
@@ -21,8 +21,11 @@ import { updateLoopStatus } from "./loop-status"
|
|
|
21
21
|
// Resolved lazily — each spawn re-reads the env var so the full-suite test
|
|
22
22
|
// run, where module load order isn't guaranteed, sees the test's override
|
|
23
23
|
// even if session.ts was imported earlier with the env var unset.
|
|
24
|
+
// The AI runs inside the linux sandbox, so it needs the linux claude binary
|
|
25
|
+
// (resolveSandboxClaudeBinary) — on a linux host that's the host claude; on a
|
|
26
|
+
// darwin/win host it's the linux binary postinstall fetched into sandbox-claude.
|
|
24
27
|
function getClaudeBinary(): string {
|
|
25
|
-
return process.env.LOOPAT_CLAUDE_BIN ||
|
|
28
|
+
return process.env.LOOPAT_CLAUDE_BIN || resolveSandboxClaudeBinary()
|
|
26
29
|
}
|
|
27
30
|
const DEBUG = !!process.env.LOOPAT_DEBUG || !!process.env.LOOPAT_DEBUG_SPAWN
|
|
28
31
|
|