loopat 0.1.50 → 0.1.52
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 +5 -2
- package/server/src/index.ts +10 -1
- package/server/src/loops.ts +65 -12
- package/server/templates/CLAUDE.md +1 -0
- package/web/dist/assets/{CodeEditor-DtHZtsPs.js → CodeEditor-JV36Z3V5.js} +8 -8
- package/web/dist/assets/Editor-9lLPQzvP.js +1 -0
- package/web/dist/assets/Markdown-D5ElKjTG.js +5 -0
- package/web/dist/assets/{MilkdownEditor-D2-3eNpY.js → MilkdownEditor-t_CG4MJj.js} +9 -9
- package/web/dist/assets/{Terminal-trnCVajY.js → Terminal-Vi3Ufhgi.js} +2 -2
- package/web/dist/assets/index-C1GIAoyn.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-C7JCzVsf.js +0 -1
- package/web/dist/assets/Markdown-DPZuNxt-.js +0 -5
- package/web/dist/assets/index-CRS7bmLR.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.52",
|
|
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",
|
|
@@ -39,7 +39,10 @@
|
|
|
39
39
|
"test:e2e": "playwright test",
|
|
40
40
|
"test:e2e:ui": "playwright test --ui",
|
|
41
41
|
"dogfood": "playwright test --config dogfood/playwright.config.ts",
|
|
42
|
-
"dogfood:
|
|
42
|
+
"dogfood:smoke": "playwright test --config dogfood/playwright.config.ts dogfood/first-5-minutes",
|
|
43
|
+
"dogfood:journey": "playwright test --config dogfood/first-run/playwright.config.ts",
|
|
44
|
+
"dogfood:first-run": "playwright test --config dogfood/first-run/playwright.config.ts",
|
|
45
|
+
"dogfood:sync": "playwright test --config dogfood/sync/playwright.config.ts"
|
|
43
46
|
},
|
|
44
47
|
"dependencies": {
|
|
45
48
|
"@anthropic-ai/claude-agent-sdk": "^0.3.150",
|
package/server/src/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { createBunWebSocket } from "hono/bun"
|
|
|
4
4
|
import { existsSync } from "node:fs"
|
|
5
5
|
import { execFile, execFileSync } from "node:child_process"
|
|
6
6
|
import { promisify } from "node:util"
|
|
7
|
-
import { listLoops, createLoop, getLoop, loopExists, patchLoopMeta, backfillAllMounts, ensureWorkspaceDirs, provisionUserPersonal, importPersonalFromRepo, setupPersonalViaProvider, listPersonalReposViaProvider, authenticateViaProvider, isPersonalFresh, ensureUiNotesWorktree, syncUiNotes, ffUpdateUiNotes, notesBehind, inspectPersonalDirty, syncPersonalToRemote, deletePersonalVault, pullPersonalFromRemote, pushPersonalToRemote, ensureContextMounts, effectiveDriver, isDriver, distillLoop, inspectRepoSync, pullRepoFromRemote, pushRepoToRemote, listVaultPublicKeys, userOnboarding, submitOnboarding } from "./loops"
|
|
7
|
+
import { listLoops, createLoop, getLoop, loopExists, patchLoopMeta, backfillAllMounts, ensureWorkspaceDirs, provisionUserPersonal, importPersonalFromRepo, setupPersonalViaProvider, listPersonalReposViaProvider, authenticateViaProvider, isPersonalFresh, ensureUiNotesWorktree, syncUiNotes, ffUpdateUiNotes, discardUiNotes, notesBehind, inspectPersonalDirty, syncPersonalToRemote, deletePersonalVault, pullPersonalFromRemote, pushPersonalToRemote, ensureContextMounts, effectiveDriver, isDriver, distillLoop, inspectRepoSync, pullRepoFromRemote, pushRepoToRemote, listVaultPublicKeys, userOnboarding, submitOnboarding } from "./loops"
|
|
8
8
|
import { getEphemeralHostPort, probePodman, stopAllWorkspaceContainers, ensureServeContainer, ensurePortProxyContainer, ensureSandboxImage } from "./podman"
|
|
9
9
|
import { startMcpAuth, completeMcpAuth, probeOAuthSupport, evictOAuthProbe, parseBearerEnvName, mcpRequiredEnvs, parseTemplateVars, type OAuthSupport } from "./mcp-oauth"
|
|
10
10
|
import { DEFAULT_VAULT, loadVaultEnvs } from "./vaults"
|
|
@@ -2392,6 +2392,15 @@ app.post("/api/notes/refresh", requireAuth, async (c) => {
|
|
|
2392
2392
|
return c.json({ ok: true })
|
|
2393
2393
|
})
|
|
2394
2394
|
|
|
2395
|
+
// Take-remote: drop held-back local notes edits, reset hard to origin. The
|
|
2396
|
+
// escape hatch when save reports a same-spot conflict it can't rebase past.
|
|
2397
|
+
app.post("/api/notes/discard", requireAuth, async (c) => {
|
|
2398
|
+
const userId = c.get("userId") as string
|
|
2399
|
+
const r = await discardUiNotes(userId)
|
|
2400
|
+
if (!r.ok) return c.json({ error: r.error }, 400)
|
|
2401
|
+
return c.json({ ok: true })
|
|
2402
|
+
})
|
|
2403
|
+
|
|
2395
2404
|
app.delete("/api/workspace/file", requireAuth, async (c) => {
|
|
2396
2405
|
const userId = c.get("userId") as string
|
|
2397
2406
|
const vault = c.req.query("vault") ?? ""
|
package/server/src/loops.ts
CHANGED
|
@@ -453,8 +453,23 @@ async function ensureRepoMirror(user: string, name: string, sshCommand?: string)
|
|
|
453
453
|
if (!spec?.git) return null
|
|
454
454
|
const dir = personalRepoCacheDir(user, name)
|
|
455
455
|
const env = sshCommand ? { ...process.env, GIT_SSH_COMMAND: sshCommand } : process.env
|
|
456
|
-
//
|
|
456
|
+
// Pin the STANDARD fetch refspec (default branch → refs/remotes/origin/<def>)
|
|
457
|
+
// so worktrees off this mirror get an ordinary `origin/<def>` tracking ref —
|
|
458
|
+
// `git rebase origin/<def>`, `git status` ahead/behind, `git log origin/<def>`
|
|
459
|
+
// all work as in any normal clone. Self-healing: re-assert it on EVERY ensure
|
|
460
|
+
// (incl. caches pinned by the old non-standard `+…:refs/heads/<def>` refspec),
|
|
461
|
+
// then fetch so the standard ref materializes.
|
|
462
|
+
const assertStandardRefspec = async () => {
|
|
463
|
+
const def = await execFileP("git", ["-C", dir, "symbolic-ref", "--short", "HEAD"])
|
|
464
|
+
.then((r) => r.stdout.trim()).catch(() => "")
|
|
465
|
+
if (def) {
|
|
466
|
+
await execFileP("git", ["-C", dir, "config", "remote.origin.fetch", `+refs/heads/${def}:refs/remotes/origin/${def}`]).catch(() => {})
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
// Already mirrored → re-assert the standard refspec, then fetch (incremental,
|
|
470
|
+
// fast). HEAD presence == bare repo.
|
|
457
471
|
if (existsSyncBase(join(dir, "HEAD"))) {
|
|
472
|
+
await assertStandardRefspec()
|
|
458
473
|
await execFileP("git", ["-C", dir, "fetch", "--quiet", "origin"], { env, timeout: 60_000 }).catch(() => {})
|
|
459
474
|
return dir
|
|
460
475
|
}
|
|
@@ -474,13 +489,13 @@ async function ensureRepoMirror(user: string, name: string, sshCommand?: string)
|
|
|
474
489
|
await execFileP("git", ["clone", "--bare", "--single-branch", "--", spec.git, dir], { env, timeout: 300_000 })
|
|
475
490
|
}
|
|
476
491
|
// A bare clone sets NO fetch refspec, so `git fetch origin` wouldn't advance
|
|
477
|
-
// any ref. Pin one for JUST the default branch
|
|
478
|
-
// refs/heads/<
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
}
|
|
492
|
+
// any ref. Pin the STANDARD one for JUST the default branch
|
|
493
|
+
// (+refs/heads/<def>:refs/remotes/origin/<def>) so worktrees off this mirror
|
|
494
|
+
// get an ordinary `origin/<def>` tracking ref — and the mirror stays small.
|
|
495
|
+
await assertStandardRefspec()
|
|
496
|
+
// Materialize refs/remotes/origin/<def> now (the clone only wrote
|
|
497
|
+
// refs/heads/<def>), so the very first worktree already tracks origin.
|
|
498
|
+
await execFileP("git", ["-C", dir, "fetch", "--quiet", "origin"], { env, timeout: 60_000 }).catch(() => {})
|
|
484
499
|
console.log(`[loopat] mirrored ${spec.git} → ${dir}`)
|
|
485
500
|
return dir
|
|
486
501
|
} catch (e: any) {
|
|
@@ -1592,6 +1607,29 @@ export async function ffUpdateUiNotes(
|
|
|
1592
1607
|
}
|
|
1593
1608
|
}
|
|
1594
1609
|
|
|
1610
|
+
/**
|
|
1611
|
+
* Take-remote: discard the user's held-back notes edits and reset hard to
|
|
1612
|
+
* origin. The escape hatch from a same-spot conflict the rebase can't clear —
|
|
1613
|
+
* "drop this edit, take the remote" (the other half is keep-mine via a loop AI
|
|
1614
|
+
* merge). Destructive by design; the UI confirms first.
|
|
1615
|
+
*/
|
|
1616
|
+
export async function discardUiNotes(
|
|
1617
|
+
user: string,
|
|
1618
|
+
): Promise<{ ok: true } | { ok: false; error: string }> {
|
|
1619
|
+
const dir = uiNotesDir(user)
|
|
1620
|
+
await ensureUiNotesWorktree(user)
|
|
1621
|
+
const branch = await remoteDefaultBranch(dir)
|
|
1622
|
+
try {
|
|
1623
|
+
await execFileP("git", ["-C", dir, "fetch", "origin"], {
|
|
1624
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GIT_SSH_COMMAND: sshCommandForUser(user) }, timeout: 30_000,
|
|
1625
|
+
})
|
|
1626
|
+
await execFileP("git", ["-C", dir, "reset", "--hard", `origin/${branch}`])
|
|
1627
|
+
return { ok: true }
|
|
1628
|
+
} catch (e: any) {
|
|
1629
|
+
return { ok: false, error: `discard failed: ${e?.stderr ?? e?.message ?? e}` }
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1595
1633
|
/**
|
|
1596
1634
|
* How many commits the user's notes worktree is behind origin/main (after a
|
|
1597
1635
|
* fetch). Drives the "remote updated" hint. 0 = up to date.
|
|
@@ -2059,6 +2097,14 @@ async function remoteDefaultBranch(dir: string): Promise<string> {
|
|
|
2059
2097
|
const m = stdout.match(/ref:\s+refs\/heads\/(\S+)\s+HEAD/)
|
|
2060
2098
|
if (m?.[1]) return m[1]
|
|
2061
2099
|
} catch {}
|
|
2100
|
+
// Bare mirrors don't carry a refs/remotes/origin/HEAD, but their own HEAD
|
|
2101
|
+
// symbolic-ref names the default branch the clone tracked — cheaper than (and
|
|
2102
|
+
// a fallback for) ls-remote, and correct for `ensureRepoMirror`'s mirrors.
|
|
2103
|
+
try {
|
|
2104
|
+
const { stdout } = await execFileP("git", ["-C", dir, "symbolic-ref", "--short", "HEAD"])
|
|
2105
|
+
const b = stdout.trim().replace(/^origin\//, "")
|
|
2106
|
+
if (b) return b
|
|
2107
|
+
} catch {}
|
|
2062
2108
|
return "main"
|
|
2063
2109
|
}
|
|
2064
2110
|
|
|
@@ -2206,10 +2252,17 @@ export async function createLoop(opts: {
|
|
|
2206
2252
|
const branch = `loop/${(await shortBranchSlug(meta.title))}-${id.slice(0, 6)}`
|
|
2207
2253
|
try {
|
|
2208
2254
|
// ① pull (docs/context-flow.md): ensureRepoMirror already fetched, so the
|
|
2209
|
-
// mirror's
|
|
2210
|
-
// the
|
|
2211
|
-
//
|
|
2212
|
-
|
|
2255
|
+
// mirror's `origin/<default>` tracking ref is the latest consensus. Open
|
|
2256
|
+
// the loop branch off `origin/<default>` (not the bare mirror's HEAD) so
|
|
2257
|
+
// the worktree is an ORDINARY clone: it has a real `origin/<default>`
|
|
2258
|
+
// tracking ref and `git rebase origin/<default>` / `git status`
|
|
2259
|
+
// ahead-behind work with nothing special to learn. Fall back to HEAD only
|
|
2260
|
+
// if origin/<default> can't be resolved (offline / empty remote).
|
|
2261
|
+
const start = await remoteStartPoint(repoPath, userSsh)
|
|
2262
|
+
const addArgs = start
|
|
2263
|
+
? ["-C", repoPath, "worktree", "add", "-b", branch, loopWorkdir(id), start]
|
|
2264
|
+
: ["-C", repoPath, "worktree", "add", "-b", branch, loopWorkdir(id)]
|
|
2265
|
+
await execFileP("git", addArgs)
|
|
2213
2266
|
meta.repo = opts.repo
|
|
2214
2267
|
meta.branch = branch
|
|
2215
2268
|
} catch (e: any) {
|
|
@@ -71,6 +71,7 @@ For team memory: when an insight is genuinely team-relevant (a convention everyo
|
|
|
71
71
|
- **Don't echo sensitive values** (API keys, tokens, SSH key material, anything that looks like a credential) to chat. Reference by filename or env var name instead.
|
|
72
72
|
- **Default to short, direct answers**. Don't announce a plan unless the task is genuinely large.
|
|
73
73
|
- **Read before Edit on long files**; avoid guessing surrounding context.
|
|
74
|
+
- **`origin` is the source of truth — finishing means pushing to origin.** The workdir is an ordinary git worktree with a normal `origin/<default>` tracking ref, so `git rebase origin/<default>`, `git status` ahead/behind, and `git log origin/<default>` all work as usual. A local commit is NOT "done": work is only preserved and shared once it reaches origin — open a PR, or push directly when that's the team's flow. Don't consider a task complete while it lives only in local commits.
|
|
74
75
|
|
|
75
76
|
## collaboration
|
|
76
77
|
|