@nyte-ai/host 0.0.23 → 0.0.25-beta.1

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 (61) hide show
  1. package/README.md +2 -2
  2. package/THIRD-PARTY-NOTICES.md +2 -38
  3. package/dist/chunks/{canvas-preview-d97wvref.js → canvas-preview-1r9sgbj3.js} +1 -1
  4. package/dist/chunks/{canvas-preview-gm0frf4b.js → canvas-preview-2yzyt077.js} +14 -0
  5. package/dist/chunks/{canvas-preview-whfdhma6.js → canvas-preview-8sv3j0vt.js} +1 -1
  6. package/dist/chunks/{canvas-preview-e3vnkkc5.js → canvas-preview-9bxhwjrh.js} +2 -1
  7. package/dist/chunks/{canvas-preview-2jwbe14k.js → canvas-preview-bg63k8n7.js} +113 -21
  8. package/dist/chunks/{canvas-preview-9e15hp6q.js → canvas-preview-da6dwy05.js} +4 -3
  9. package/dist/chunks/{canvas-preview-ayayyd43.js → canvas-preview-dt89p9pm.js} +148 -6
  10. package/dist/src/durable-object.d.ts +1 -1
  11. package/dist/src/index.js +7 -7
  12. package/dist/src/listen-fetch.d.ts +1 -1
  13. package/dist/src/listen-fetch.js +3 -3
  14. package/dist/src/listen.js +4 -4
  15. package/dist/src/local/provider.d.ts +1 -1
  16. package/dist/src/local/provider.js +2 -2
  17. package/dist/src/local/worktrees.d.ts +7 -3
  18. package/dist/src/local/worktrees.js +1 -2
  19. package/dist/src/open.js +7 -7
  20. package/dist/src/serve/binding.d.ts +0 -1
  21. package/dist/src/serve/binding.js +1 -1
  22. package/dist/src/serve/handler.js +2 -2
  23. package/package.json +8 -31
  24. package/src/durable-object.ts +1 -1
  25. package/src/listen-fetch.ts +6 -3
  26. package/src/local/provider.ts +3 -1
  27. package/src/local/worktrees.ts +165 -30
  28. package/src/open.ts +7 -1
  29. package/src/serve/binding.ts +18 -1
  30. package/src/usage-history.ts +0 -1
  31. package/dist/chunks/canvas-preview-1f8h0he1.js +0 -81
  32. package/dist/chunks/canvas-preview-2zdyh9ny.js +0 -26
  33. package/dist/chunks/canvas-preview-65xqjw7f.js +0 -20
  34. package/dist/chunks/canvas-preview-dsb5829c.js +0 -166
  35. package/dist/chunks/canvas-preview-kzmp015w.js +0 -36
  36. package/dist/src/deploy/cloudflare/worker.d.ts +0 -50
  37. package/dist/src/deploy/cloudflare/worker.js +0 -196
  38. package/dist/src/deploy/vercel/nitro.config.d.ts +0 -2
  39. package/dist/src/deploy/vercel/nitro.config.js +0 -30
  40. package/dist/src/deploy/vercel/server/routes/[...].d.ts +0 -2
  41. package/dist/src/deploy/vercel/server/routes/[...].js +0 -18
  42. package/dist/src/deploy/vercel/server/utils/host.d.ts +0 -13
  43. package/dist/src/deploy/vercel/server/utils/host.js +0 -7
  44. package/dist/src/deploy/vercel/server/utils/repair.d.ts +0 -1
  45. package/dist/src/deploy/vercel/server/utils/repair.js +0 -6
  46. package/dist/src/deploy/vercel/server/utils/step.d.ts +0 -10
  47. package/dist/src/deploy/vercel/server/utils/step.js +0 -6
  48. package/dist/src/deploy/vercel/server/workflows/drive.d.ts +0 -14
  49. package/dist/src/deploy/vercel/server/workflows/drive.js +0 -13
  50. package/src/deploy/cloudflare/README.md +0 -3
  51. package/src/deploy/cloudflare/worker.ts +0 -306
  52. package/src/deploy/cloudflare/wrangler.jsonc +0 -12
  53. package/src/deploy/vercel/README.md +0 -3
  54. package/src/deploy/vercel/nitro.config.ts +0 -29
  55. package/src/deploy/vercel/server/routes/[...].ts +0 -10
  56. package/src/deploy/vercel/server/utils/host.ts +0 -104
  57. package/src/deploy/vercel/server/utils/repair.ts +0 -35
  58. package/src/deploy/vercel/server/utils/step.ts +0 -25
  59. package/src/deploy/vercel/server/workflows/drive.ts +0 -46
  60. package/src/deploy/vercel/tsconfig.json +0 -9
  61. package/src/deploy/vercel/types/bun.d.ts +0 -2
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  localProvider2
3
- } from "../../chunks/canvas-preview-e3vnkkc5.js";
4
- import"../../chunks/canvas-preview-2jwbe14k.js";
3
+ } from "../../chunks/canvas-preview-9bxhwjrh.js";
4
+ import"../../chunks/canvas-preview-bg63k8n7.js";
5
5
  import"../../chunks/canvas-preview-5ekjx6sw.js";
6
6
  import"../../chunks/canvas-preview-8xc0vn7v.js";
7
7
  import"../../chunks/canvas-preview-1sq7z9jq.js";
@@ -22,11 +22,15 @@ export declare function hasBranch(cwd: string, name: string, options?: {
22
22
  }): Promise<boolean>;
23
23
  /**
24
24
  * Returns git worktrees for local workspaces (`git`, default `git` on the
25
- * PATH), each under `<home>/.nyte/worktrees/<repository>/<slug>` on the branch
26
- * `nyte/<slug>`, the slug the title's words and a hash of the name.
25
+ * PATH), each under `<home>/.nyte/worktrees/<repository>/<branch>` on a branch
26
+ * named by `branchName`: at most three hyphenated words from the title.
27
27
  *
28
28
  * `create` refuses a folder outside a repository or one without a commit, then
29
- * answers the worktree already on that branch, or adds one from HEAD and
29
+ * answers the worktree on the branch whose `branch.<branch>.nyte-session`
30
+ * config is `name`, or claims the first name with no folder or listed worktree
31
+ * at its path by creating its branch at HEAD (git refuses an existing branch,
32
+ * which also settles a race with a concurrent create: the loser tries the next
33
+ * name), records `name` in that config, and adds a worktree for it; then it
30
34
  * copies the workspace's uncommitted changes in: the worktree's tracked and
31
35
  * untracked files, ignored ones aside, are written to a tree through a
32
36
  * temporary index seeded from the real one (which is never touched), and the
@@ -3,9 +3,8 @@ import {
3
3
  addWorktree2,
4
4
  hasBranch2,
5
5
  localWorktrees2
6
- } from "../../chunks/canvas-preview-2jwbe14k.js";
6
+ } from "../../chunks/canvas-preview-bg63k8n7.js";
7
7
  import"../../chunks/canvas-preview-bxnqt8cd.js";
8
- import"../../chunks/canvas-preview-0nq75h9k.js";
9
8
  export {
10
9
  addWorktree2 as addWorktree,
11
10
  hasBranch2 as hasBranch,
package/dist/src/open.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  openHost2
3
- } from "../chunks/canvas-preview-ayayyd43.js";
3
+ } from "../chunks/canvas-preview-dt89p9pm.js";
4
4
  import"../chunks/canvas-preview-4e0pxa32.js";
5
5
  import"../chunks/canvas-preview-y5y6ghaw.js";
6
6
  import"../chunks/canvas-preview-ec2mt7es.js";
7
7
  import"../chunks/canvas-preview-befkjea2.js";
8
8
  import"../chunks/canvas-preview-9aqczacs.js";
9
- import"../chunks/canvas-preview-e3vnkkc5.js";
10
- import"../chunks/canvas-preview-2jwbe14k.js";
9
+ import"../chunks/canvas-preview-9bxhwjrh.js";
10
+ import"../chunks/canvas-preview-bg63k8n7.js";
11
11
  import"../chunks/canvas-preview-5ekjx6sw.js";
12
12
  import"../chunks/canvas-preview-8xc0vn7v.js";
13
13
  import"../chunks/canvas-preview-1sq7z9jq.js";
@@ -26,10 +26,10 @@ import"../chunks/canvas-preview-s94ebwf5.js";
26
26
  import"../chunks/canvas-preview-kdqgrstc.js";
27
27
  import"../chunks/canvas-preview-mstbvwz3.js";
28
28
  import"../chunks/canvas-preview-zaaj99wd.js";
29
- import"../chunks/canvas-preview-whfdhma6.js";
30
- import"../chunks/canvas-preview-9e15hp6q.js";
31
- import"../chunks/canvas-preview-d97wvref.js";
32
- import"../chunks/canvas-preview-gm0frf4b.js";
29
+ import"../chunks/canvas-preview-8sv3j0vt.js";
30
+ import"../chunks/canvas-preview-da6dwy05.js";
31
+ import"../chunks/canvas-preview-1r9sgbj3.js";
32
+ import"../chunks/canvas-preview-2yzyt077.js";
33
33
  import"../chunks/canvas-preview-rp4x6y1h.js";
34
34
  import"../chunks/canvas-preview-skzwvw4d.js";
35
35
  import"../chunks/canvas-preview-bxnqt8cd.js";
@@ -34,6 +34,5 @@ export type Binding = CallBinding & FrameBinding & ByteBinding & OutputBinding;
34
34
  export type BindingOptions = {
35
35
  readonly server: ServerHost;
36
36
  };
37
- /** Returns each row bound to the call it names: ids bind handles on the connection, handles returned become ids, `Set`s and `Map`s become their wire forms; `head.move`, `files.batch` and `browser.open` go through the server port's wire operations with their wire id (§40o, §40x). `head.watch` sends the head's view from the host's fold whole, then each later view as its changes (`viewFrames`), the core watch marking the principal's seen tip (§40a, §54a); `sessions.watch` sends the list whole, then each later list as its order and changed rows (`sessionsFrames`). Authority is the caller's gate: the server checks each row's before binding. */
38
37
  export declare function bindConnection(connection: Connection, options: BindingOptions): Binding;
39
38
  export {};
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  bindConnection2
3
- } from "../../chunks/canvas-preview-gm0frf4b.js";
3
+ } from "../../chunks/canvas-preview-2yzyt077.js";
4
4
  export {
5
5
  bindConnection2 as bindConnection
6
6
  };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createHandler2
3
- } from "../../chunks/canvas-preview-d97wvref.js";
4
- import"../../chunks/canvas-preview-gm0frf4b.js";
3
+ } from "../../chunks/canvas-preview-1r9sgbj3.js";
4
+ import"../../chunks/canvas-preview-2yzyt077.js";
5
5
  import"../../chunks/canvas-preview-rp4x6y1h.js";
6
6
  export {
7
7
  createHandler2 as createHandler
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyte-ai/host",
3
- "version": "0.0.23",
3
+ "version": "0.0.25-beta.1",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -79,14 +79,6 @@
79
79
  "./usage-worker": {
80
80
  "types": "./dist/src/usage-worker.d.ts",
81
81
  "import": "./dist/src/usage-worker.js"
82
- },
83
- "./deploy/cloudflare": {
84
- "types": "./dist/src/deploy/cloudflare/worker.d.ts",
85
- "import": "./dist/src/deploy/cloudflare/worker.js"
86
- },
87
- "./deploy/vercel": {
88
- "types": "./dist/src/deploy/vercel/nitro.config.d.ts",
89
- "import": "./dist/src/deploy/vercel/nitro.config.js"
90
82
  }
91
83
  },
92
84
  "publishConfig": {
@@ -96,11 +88,11 @@
96
88
  "dependencies": {
97
89
  "@earendil-works/pi-mcp": "^1.3.0",
98
90
  "@lydell/node-pty": "^1.2.0-beta.15",
99
- "@nyte-ai/ai": "0.0.23",
100
- "@nyte-ai/core": "0.0.23",
101
- "@nyte-ai/plugin": "0.0.23",
102
- "@nyte-ai/protocol": "0.0.23",
103
- "@nyte-ai/schema": "0.0.23",
91
+ "@nyte-ai/ai": "0.0.25-beta.1",
92
+ "@nyte-ai/core": "0.0.25-beta.1",
93
+ "@nyte-ai/plugin": "0.0.25-beta.1",
94
+ "@nyte-ai/protocol": "0.0.25-beta.1",
95
+ "@nyte-ai/schema": "0.0.25-beta.1",
104
96
  "@silvia-odwyer/photon": "^0.3.3",
105
97
  "fflate": "^0.8.3",
106
98
  "ignore": "^7.0.13",
@@ -113,29 +105,14 @@
113
105
  "@types/node": "^26.6.5",
114
106
  "@types/pg": "^8.23.1",
115
107
  "fast-check": "^4.10.2",
116
- "nitro": "^3.0.260903-beta",
117
- "typescript": "6.0.3",
118
- "vitest": "^5.0.5",
119
- "workflow": "^5.2.3"
120
- },
121
- "peerDependencies": {
122
- "nitro": "^3.0.260903-beta",
123
- "workflow": "^5.2.3"
124
- },
125
- "peerDependenciesMeta": {
126
- "nitro": {
127
- "optional": true
128
- },
129
- "workflow": {
130
- "optional": true
131
- }
108
+ "vitest": "^5.0.5"
132
109
  },
133
110
  "engines": {
134
111
  "node": ">=26.4.0"
135
112
  },
136
113
  "scripts": {
137
114
  "clean": "rm -rf node_modules .turbo",
138
- "typecheck": "tsrs --noEmit && tsrs --noEmit -p src/deploy/vercel",
115
+ "typecheck": "tsc --noEmit",
139
116
  "test": "vitest --run",
140
117
  "check:store": "node scripts/check-store.ts",
141
118
  "build:package": "bun ../../scripts/sdk-packages.mjs build host"
@@ -44,7 +44,7 @@ const ROW_LIMIT = 2_000_000;
44
44
 
45
45
  const encoder = new TextEncoder();
46
46
 
47
- /** Returns a `Store` over a Durable Object's SQLite storage, node-free, with the sqlite store's tables, row helpers (`store-rows.ts`) and semantics (§41f): `objects(id TEXT PRIMARY KEY, body TEXT NULL, bytes BLOB NULL)`, `refs(name TEXT PRIMARY KEY, target TEXT NULL, revision TEXT NOT NULL)` and `log(session TEXT, seq INTEGER, at INTEGER, event TEXT, PRIMARY KEY (session, seq))`, brought to core's `STORE_VERSION` by `migrateStore` on first `open()`, each commit one `transactionSync` and no backup; the open rejects with `SdkError("unavailable")` for a store a newer Nyte migrated. `put` ids the value with core's `objectId` and stores its JSON once; `putBlob` buffers the stream, ids it with `blobId` and stores the bytes once as an `ArrayBuffer`; an object and a blob that share an id share the row, each column filled once; `blob` reads the bytes back with their size and a null mime; a body, blob or logged event past the Durable Object's 2 MB row limit throws `SdkError("invalid")` before anything is written (§42k). `refs.set` runs inside one `transactionSync` with no await between its reads and writes, first rejecting with `SdkError("unavailable")` when a newer Nyte has migrated the store, with `RefStore.set`'s semantics, each revision a random UUID, any throw inside rolling the whole batch back. `list(prefix)` reads the name range from `prefix` in name order; `log` returns the events after `after` in seq order up to `limit`, each validated as a `SessionEvent`; `tail` is the session's last seq, or 0. Every `open()` returns a connection over the one storage; `close()` releases nothing, the storage being the object's. In workerd, which allows code generation only while modules load, the Worker turns TypeBox acceleration off at startup (`Settings.Set({ useAcceleration: false })` from `typebox/system`, as `deploy/cloudflare/worker.ts` does), or `log`'s validation throws inside a request. */
47
+ /** Returns a `Store` over a Durable Object's SQLite storage, node-free, with the sqlite store's tables, row helpers (`store-rows.ts`) and semantics (§41f): `objects(id TEXT PRIMARY KEY, body TEXT NULL, bytes BLOB NULL)`, `refs(name TEXT PRIMARY KEY, target TEXT NULL, revision TEXT NOT NULL)` and `log(session TEXT, seq INTEGER, at INTEGER, event TEXT, PRIMARY KEY (session, seq))`, brought to core's `STORE_VERSION` by `migrateStore` on first `open()`, each commit one `transactionSync` and no backup; the open rejects with `SdkError("unavailable")` for a store a newer Nyte migrated. `put` ids the value with core's `objectId` and stores its JSON once; `putBlob` buffers the stream, ids it with `blobId` and stores the bytes once as an `ArrayBuffer`; an object and a blob that share an id share the row, each column filled once; `blob` reads the bytes back with their size and a null mime; a body, blob or logged event past the Durable Object's 2 MB row limit throws `SdkError("invalid")` before anything is written (§42k). `refs.set` runs inside one `transactionSync` with no await between its reads and writes, first rejecting with `SdkError("unavailable")` when a newer Nyte has migrated the store, with `RefStore.set`'s semantics, each revision a random UUID, any throw inside rolling the whole batch back. `list(prefix)` reads the name range from `prefix` in name order; `log` returns the events after `after` in seq order up to `limit`, each validated as a `SessionEvent`; `tail` is the session's last seq, or 0. Every `open()` returns a connection over the one storage; `close()` releases nothing, the storage being the object's. In workerd, which allows code generation only while modules load, the Worker turns TypeBox acceleration off at startup (`Settings.Set({ useAcceleration: false })` from `typebox/system`, as `packages/examples/cloudflare/worker.ts` does), or `log`'s validation throws inside a request. */
48
48
  export function durableObjectStore(storage: DurableObjectSql): Store {
49
49
  let connection: StoreConnection | null = null;
50
50
 
@@ -36,7 +36,7 @@ type Device = {
36
36
 
37
37
  const encoder = new TextEncoder();
38
38
 
39
- /** Serves a composed host as a bare fetch handler with no socket and no `node:*` import, so a Worker can serve it (§41j): `before`, the unauthenticated identity and describe routes, then `createHandler` over one cached connection per principal and authority, every response carrying `X-Nyte-Host` (§43a, §43j). A request authenticates by a pairing code, then the listener token (compared in constant time over SHA-256 digests) or a device token remembered until a commit from another process moves the host's principals, then `authorize` under `custom`; a revoke aborts that principal's open requests and streams. A `token` shorter than 16 characters throws `SdkError("invalid")`; `execution` (default: run covering all) drives covered sessions in process on an `admit-only` host, covers their leases otherwise and takes nothing on a `dispatch` host (§42v); `close()` disconnects the clients and releases the execution once, after which every request answers `unavailable`. */
39
+ /** Serves a composed host as a bare fetch handler with no socket and no `node:*` import, so a Worker can serve it (§41j): `before`, the unauthenticated identity and describe routes, then `createHandler` over one cached connection per principal and authority, every response carrying `X-Nyte-Host` (§43a, §43j). A request authenticates by a pairing code, then the listener token (compared in constant time over SHA-256 digests, granting `auth.authority`) or a device token remembered until a commit from another process moves the host's principals, then `authorize` under `custom`; a revoke aborts that principal's open requests and streams. A `token` shorter than 16 characters throws `SdkError("invalid")`; `execution` (default: run covering all) drives covered sessions in process on an `admit-only` host, covers their leases otherwise and takes nothing on a `dispatch` host (§42v); `close()` disconnects the clients and releases the execution once, after which every request answers `unavailable`. */
40
40
  export async function listenFetch(
41
41
  host: ServedHost,
42
42
  options: ListenOptions & { readonly transport: { readonly kind: "fetch" } },
@@ -161,6 +161,9 @@ export async function listenFetch(
161
161
  const isListenerToken: (token: string) => Promise<boolean> =
162
162
  auth.kind === "token" ? (token) => sameSecret(token, auth.token) : async () => false;
163
163
 
164
+ const listenerAuthority =
165
+ auth.kind === "token" ? (auth.authority ?? TOKEN_AUTHORITY) : TOKEN_AUTHORITY;
166
+
164
167
  const fallback: (request: Request, token: string | undefined) => Promise<Verdict> =
165
168
  auth.kind === "custom"
166
169
  ? async (request) => {
@@ -207,8 +210,8 @@ export async function listenFetch(
207
210
  if (token !== undefined) {
208
211
  if (await isListenerToken(token))
209
212
  return {
210
- connection: connectionFor(flight, TOKEN, TOKEN_AUTHORITY, token),
211
- authority: TOKEN_AUTHORITY,
213
+ connection: connectionFor(flight, TOKEN, listenerAuthority, token),
214
+ authority: listenerAuthority,
212
215
  };
213
216
  const remembered = await findDevice(token);
214
217
 
@@ -18,7 +18,7 @@ const PLUGIN_FILE = /\.(?:ts|mts|js|mjs)$/;
18
18
 
19
19
  const ROOT_SOURCES = [".nyte/SYSTEM.md", ".nyte/APPEND_SYSTEM.md", ".nyte/mcp.json"];
20
20
 
21
- /** Returns the local environment provider (`kind: "local"`): a folder on this machine. `inspect` answers a `{ kind: "local", path }` target's `folderIdentity` (§41d), `available` when the path is a readable directory (else `unavailable { unreachable }`; a target without a path `unavailable { unsupported }`, identified by the hash of its locator), and the project sources present under it, relative to it, in this order: the plugin files `.nyte/plugins/<name>.{ts,mts,js,mjs}` and `.nyte/plugins/<name>/index.<ext>`, the skills `.nyte/skills/<name>/SKILL.md` and `.agents/skills/<name>/SKILL.md`, the prompts `.nyte/prompts/<name>.md`, then `.nyte/SYSTEM.md`, `.nyte/APPEND_SYSTEM.md` and `.nyte/mcp.json`: the files project trust gates. Context files such as `AGENTS.md` load without trust and are not listed. `attach` resolves the real path (a target that is not a directory throws `SdkError("unavailable")`) and returns an `ExecutionEnv` with that identity, `cwd` the real path, `files` from `localFiles` (with `ripgrep` and `formatter`), `exec` from `localExec`, and a `close` that ends its file watches and the jobs' readers, leaving running jobs alone. `git` is `localGit` (with `git`), `github` is `localGitHub` (with `gh` and `git`; the login is the machine's, `localGitHubAccount`), `snapshots` is `localSnapshots` storing tree walks under `<home>/.nyte/snapshots` (`home` default `os.homedir()`), and absent for a folder that contains `<home>/.nyte` or lies inside it outside `<home>/.nyte/worktrees` (the home folder itself, its ancestors, Nyte's own state): there a snapshot would walk or stage everything under home, the store and secrets included, to report changes in the store it just wrote; `worktrees` is `localWorktrees` (with `home` and `git`); no `browser` (the desktop wraps one in); `destroy` rejects with `SdkError("unavailable")`: a local folder is never torn down. */
21
+ /** Returns the local environment provider (`kind: "local"`): a folder on this machine. `inspect` answers a `{ kind: "local", path }` target's `folderIdentity` (§41d), `available` when the path is a readable directory (else `unavailable { unreachable }`; a target without a path `unavailable { unsupported }`, identified by the hash of its locator), and the project sources present under it, relative to it, in this order: the plugin files `.nyte/plugins/<name>.{ts,mts,js,mjs}` and `.nyte/plugins/<name>/index.<ext>`, the skills `.nyte/skills/<name>/SKILL.md` and `.agents/skills/<name>/SKILL.md`, the prompts `.nyte/prompts/<name>.md`, then `.nyte/SYSTEM.md`, `.nyte/APPEND_SYSTEM.md` and `.nyte/mcp.json`: the files project trust gates. Context files such as `AGENTS.md` load without trust and are not listed. `attach` resolves the real path (a target that is not a directory throws `SdkError("unavailable")`) and returns an `ExecutionEnv` with that identity, `cwd` the real path, `files` from `localFiles` (with `ripgrep` and `formatter`), `exec` from `localExec`, and a `close` that ends its file watches and the jobs' readers, leaving running jobs alone. `git` is `localGit` (with `git`), `github` is `localGitHub` (with `gh` and `git`; the login is the machine's, `localGitHubAccount`), `snapshots` is `localSnapshots` storing tree walks under `<home>/.nyte/snapshots` (`home` default `os.homedir()`), and absent for a folder that contains `<home>/.nyte` or lies inside it outside `<home>/.nyte/worktrees` (the home folder itself, its ancestors, Nyte's own state): there a snapshot would walk or stage everything under home, the store and secrets included, to report changes in the store it just wrote; `worktrees` is `localWorktrees` (with `home` and `git`); `realpath` is `fs.promises.realpath` (the native one, which spells each name as the disk stores it), a relative path resolved against `env.cwd`, null where it fails; no `browser` (the desktop wraps one in); `destroy` rejects with `SdkError("unavailable")`: a local folder is never torn down. */
22
22
  export function localProvider(options?: {
23
23
  readonly home?: string;
24
24
  readonly git?: string;
@@ -95,6 +95,8 @@ export function localProvider(options?: {
95
95
  : localSnapshots(env, { store, git: options?.git });
96
96
  },
97
97
  worktrees: localWorktrees({ home, git: options?.git }),
98
+ realpath: (env, path) =>
99
+ realpath(isAbsolute(path) ? path : join(env.cwd, path)).catch(() => null),
98
100
  async destroy() {
99
101
  throw new SdkError("unavailable", "A local folder is never torn down.");
100
102
  },
@@ -1,15 +1,65 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { copyFile, mkdir, realpath, rm, stat } from "node:fs/promises";
3
3
  import { homedir, tmpdir } from "node:os";
4
- import { basename, dirname, join, resolve } from "node:path";
4
+ import { basename, join, resolve } from "node:path";
5
5
  import { SdkError } from "@nyte-ai/schema";
6
6
  import type { WorkspaceTarget } from "@nyte-ai/schema";
7
7
  import type { WorktreeProvider } from "@nyte-ai/core";
8
- import { sha256 } from "../hash.ts";
9
8
  import { run } from "./command.ts";
10
9
 
11
10
  const ENV: Readonly<Record<string, string>> = { GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" };
12
11
 
12
+ /** The branch config key (`branch.<name>.nyte-session`) naming the session a worktree's branch was made for; `git branch -D` drops it with the branch. */
13
+ const SESSION_KEY = "nyte-session";
14
+
15
+ const STOPWORDS = new Set([
16
+ "a",
17
+ "an",
18
+ "the",
19
+ "and",
20
+ "or",
21
+ "of",
22
+ "to",
23
+ "in",
24
+ "on",
25
+ "for",
26
+ "with",
27
+ "from",
28
+ "by",
29
+ "at",
30
+ "into",
31
+ "as",
32
+ "is",
33
+ "are",
34
+ "be",
35
+ "it",
36
+ "its",
37
+ "this",
38
+ "that",
39
+ "my",
40
+ "our",
41
+ "your",
42
+ ]);
43
+
44
+ const TYPE_WORDS = new Set([
45
+ "feat",
46
+ "feature",
47
+ "fix",
48
+ "add",
49
+ "chore",
50
+ "refactor",
51
+ "docs",
52
+ "test",
53
+ "perf",
54
+ "build",
55
+ "ci",
56
+ "revert",
57
+ "update",
58
+ ]);
59
+
60
+ /** How many names `create` tries before giving up: `<words>`, then `<two words>-2` up to this. */
61
+ const MAX_ATTEMPTS = 100;
62
+
13
63
  /** One checkout of a repository: its folder and the branch it has out, null when HEAD is detached. */
14
64
  export interface ListedWorktree {
15
65
  readonly path: string;
@@ -81,11 +131,15 @@ export async function hasBranch(
81
131
 
82
132
  /**
83
133
  * Returns git worktrees for local workspaces (`git`, default `git` on the
84
- * PATH), each under `<home>/.nyte/worktrees/<repository>/<slug>` on the branch
85
- * `nyte/<slug>`, the slug the title's words and a hash of the name.
134
+ * PATH), each under `<home>/.nyte/worktrees/<repository>/<branch>` on a branch
135
+ * named by `branchName`: at most three hyphenated words from the title.
86
136
  *
87
137
  * `create` refuses a folder outside a repository or one without a commit, then
88
- * answers the worktree already on that branch, or adds one from HEAD and
138
+ * answers the worktree on the branch whose `branch.<branch>.nyte-session`
139
+ * config is `name`, or claims the first name with no folder or listed worktree
140
+ * at its path by creating its branch at HEAD (git refuses an existing branch,
141
+ * which also settles a race with a concurrent create: the loser tries the next
142
+ * name), records `name` in that config, and adds a worktree for it; then it
89
143
  * copies the workspace's uncommitted changes in: the worktree's tracked and
90
144
  * untracked files, ignored ones aside, are written to a tree through a
91
145
  * temporary index seeded from the real one (which is never touched), and the
@@ -126,6 +180,77 @@ export function localWorktrees(options?: {
126
180
  }
127
181
  };
128
182
 
183
+ const findClaimedBranch = async (root: string, name: string): Promise<string | null> => {
184
+ const out = await git(root, [
185
+ "config",
186
+ "-z",
187
+ "--get-regexp",
188
+ `^branch\\..*\\.${SESSION_KEY}$`,
189
+ ]).catch(() => "");
190
+
191
+ for (const entry of out.split("\0")) {
192
+ const newline = entry.indexOf("\n");
193
+ const branch = entry.slice("branch.".length, newline - SESSION_KEY.length - 1);
194
+
195
+ if (
196
+ newline > 0 &&
197
+ entry.slice(newline + 1) === name &&
198
+ (await hasBranch(root, branch, options))
199
+ )
200
+ return branch;
201
+ }
202
+
203
+ return null;
204
+ };
205
+
206
+ const claimBranch = async (
207
+ root: string,
208
+ parent: string,
209
+ listed: readonly ListedWorktree[],
210
+ input: { readonly name: string; readonly title: string | null },
211
+ head: string,
212
+ ): Promise<string> => {
213
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
214
+ const branch = branchName(input.title, attempt);
215
+ const folder = join(parent, branch);
216
+
217
+ if (
218
+ listed.some((worktree) => worktree.path === folder) ||
219
+ (await stat(folder).catch(() => null)) !== null
220
+ )
221
+ continue;
222
+
223
+ const [created] = await Promise.allSettled([
224
+ git(root, ["branch", "--no-track", branch, head]),
225
+ ]);
226
+
227
+ if (created.status === "rejected") {
228
+ if (await hasBranch(root, branch, options)) continue;
229
+ throw created.reason;
230
+ }
231
+
232
+ await writeConfig(root, `branch.${branch}.${SESSION_KEY}`, input.name);
233
+
234
+ return branch;
235
+ }
236
+
237
+ throw new SdkError(
238
+ "unavailable",
239
+ `Every worktree name for "${input.title ?? input.name}" is taken in ${root}.`,
240
+ );
241
+ };
242
+
243
+ const writeConfig = async (root: string, key: string, value: string): Promise<void> => {
244
+ for (let attempt = 1; ; attempt++) {
245
+ const [written] = await Promise.allSettled([git(root, ["config", key, value])]);
246
+
247
+ if (written.status === "fulfilled") return;
248
+
249
+ if (attempt >= 20) throw written.reason;
250
+ await new Promise((settle) => setTimeout(settle, 10 * attempt));
251
+ }
252
+ };
253
+
129
254
  return {
130
255
  async create(env, input) {
131
256
  const repo = await readRepository(git, env.cwd);
@@ -143,42 +268,41 @@ export function localWorktrees(options?: {
143
268
  `${repo.root} has no commits yet, so a worktree has nothing to branch from.`,
144
269
  );
145
270
 
146
- const slug = worktreeSlug(input.name, input.title);
147
- const branch = `nyte/${slug}`;
148
271
  const below = (root: string): WorkspaceTarget => ({
149
272
  kind: "local",
150
273
  path: resolve(root, repo.prefix),
151
274
  });
152
275
 
153
- const listed = (await listWorktrees(repo.root, options)).find(
154
- (worktree) => worktree.branch === branch,
155
- );
276
+ const listed = await listWorktrees(repo.root, options);
277
+ const claimed = await findClaimedBranch(repo.root, input.name);
278
+ const open = listed.find((worktree) => claimed !== null && worktree.branch === claimed);
156
279
 
157
- if (listed !== undefined)
280
+ if (claimed !== null && open !== undefined)
158
281
  return {
159
- target: below(listed.path),
160
- branch,
161
- base: (await commitAt(repo.root, `refs/heads/${branch}`)) ?? head,
282
+ target: below(open.path),
283
+ branch: claimed,
284
+ base: (await commitAt(repo.root, `refs/heads/${claimed}`)) ?? head,
162
285
  };
163
286
 
164
- const folder = join(folders, basename(repo.root), slug);
165
- const branched = await hasBranch(repo.root, branch, options);
166
- const base = branched ? ((await commitAt(repo.root, `refs/heads/${branch}`)) ?? head) : head;
287
+ const parent = join(folders, basename(repo.root));
288
+ await mkdir(parent, { recursive: true });
289
+ const realParent = await realpath(parent);
290
+ const branch = claimed ?? (await claimBranch(repo.root, realParent, listed, input, head));
291
+
292
+ const base =
293
+ claimed === null ? head : ((await commitAt(repo.root, `refs/heads/${claimed}`)) ?? head);
294
+
295
+ const folder = join(realParent, branch);
167
296
  const tree = await workingTree(repo);
168
297
 
169
- await mkdir(dirname(folder), { recursive: true });
170
- await addWorktree(
171
- repo.root,
172
- branched ? { path: folder, branch } : { path: folder, branch, from: head },
173
- options,
174
- );
298
+ await addWorktree(repo.root, { path: folder, branch }, options);
175
299
 
176
300
  if (tree !== (await git(folder, ["rev-parse", "HEAD^{tree}"])).trim()) {
177
301
  await git(folder, ["read-tree", "-m", "-u", "HEAD", tree]);
178
302
  await git(folder, ["reset", "-q"]);
179
303
  }
180
304
 
181
- return { target: below(await realpath(folder)), branch, base };
305
+ return { target: below(folder), branch, base };
182
306
  },
183
307
  async remove(worktree) {
184
308
  const path = "path" in worktree.target ? worktree.target.path : null;
@@ -227,13 +351,24 @@ async function readRepository(git: Git, cwd: string): Promise<Repository | null>
227
351
  : { root: resolve(root), gitDir: resolve(gitDir), prefix };
228
352
  }
229
353
 
230
- function worktreeSlug(name: string, title: string | null): string {
354
+ /**
355
+ * Names a worktree's branch from its title: lowercase ASCII words, stopwords
356
+ * dropped, and type words such as `fix` or `add` too while two words are left
357
+ * without them; three words on the first attempt, then two and `-<attempt>`,
358
+ * so a name never has more than three hyphenated parts. `worktree` stands in
359
+ * for a title with no words.
360
+ */
361
+ function branchName(title: string | null, attempt: number): string {
231
362
  const words = (title ?? "")
363
+ .normalize("NFKD")
232
364
  .toLowerCase()
233
- .replace(/[^a-z0-9]+/gu, "-")
234
- .slice(0, 40)
235
- .replace(/^-+|-+$/gu, "");
236
- const key = sha256(name).slice(0, 8);
365
+ .split(/[^a-z0-9]+/u)
366
+ .filter((word) => word !== "");
367
+
368
+ const kept = words.filter((word) => !STOPWORDS.has(word));
369
+ const content = kept.filter((word) => !TYPE_WORDS.has(word));
370
+ const chosen = content.length >= 2 ? content : kept.length > 0 ? kept : words;
371
+ const stem = chosen.length === 0 ? "worktree" : chosen.slice(0, attempt === 1 ? 3 : 2).join("-");
237
372
 
238
- return words === "" ? key : `${words}-${key}`;
373
+ return attempt === 1 ? stem : `${stem}-${attempt}`;
239
374
  }
package/src/open.ts CHANGED
@@ -50,6 +50,8 @@ import { defaultSecrets } from "./secrets.ts";
50
50
  import type { SecretsSource } from "./secrets.ts";
51
51
  import { sqliteStore, storePath } from "./sqlite.ts";
52
52
 
53
+ declare const NYTE_BUILT_VERSION: string | undefined;
54
+
53
55
  const RELOAD_DEBOUNCE_MS = 250;
54
56
 
55
57
  const STORE_POLL_MS = 500;
@@ -441,7 +443,11 @@ async function settingsSeed(home: string): Promise<{
441
443
  }
442
444
 
443
445
  function hostDescription(): HostDescription {
444
- return { name: hostname(), platform: process.platform, version: manifest.version };
446
+ return {
447
+ name: hostname(),
448
+ platform: process.platform,
449
+ version: typeof NYTE_BUILT_VERSION === "string" ? NYTE_BUILT_VERSION : manifest.version,
450
+ };
445
451
  }
446
452
 
447
453
  function settingsPath(home: string): string {
@@ -103,7 +103,6 @@ const attempts = new WeakMap<ServerHost, Map<string, Attempt>>();
103
103
 
104
104
  const holds = new WeakMap<ServerHost, Map<string, Page[]>>();
105
105
 
106
- /** Returns each row bound to the call it names: ids bind handles on the connection, handles returned become ids, `Set`s and `Map`s become their wire forms; `head.move`, `files.batch` and `browser.open` go through the server port's wire operations with their wire id (§40o, §40x). `head.watch` sends the head's view from the host's fold whole, then each later view as its changes (`viewFrames`), the core watch marking the principal's seen tip (§40a, §54a); `sessions.watch` sends the list whole, then each later list as its order and changed rows (`sessionsFrames`). Authority is the caller's gate: the server checks each row's before binding. */
107
106
  export function bindConnection(connection: Connection, options: BindingOptions): Binding {
108
107
  const { wire } = options.server;
109
108
  const { principal } = connection;
@@ -285,6 +284,11 @@ export function bindConnection(connection: Connection, options: BindingOptions):
285
284
 
286
285
  return viewFrames(session(id).head(head).watch(options));
287
286
  },
287
+ "head.see": async ({ session: id, head, commit }) => {
288
+ await session(id).head(head).see(commit);
289
+
290
+ return null;
291
+ },
288
292
  "head.history": ({ session: id, head, before, limit }) =>
289
293
  session(id).head(head).history({ before, limit }),
290
294
  "head.tree": ({ session: id, head }) => session(id).head(head).tree(),
@@ -293,6 +297,14 @@ export function bindConnection(connection: Connection, options: BindingOptions):
293
297
  }),
294
298
  "head.compact": ({ session: id, head, instructions }) =>
295
299
  session(id).head(head).compact({ instructions }),
300
+ "head.revise": ({ session: id, head, message, content, options }) =>
301
+ session(id).head(head).revise(message, content, options),
302
+ "head.hold": ({ session: id, head, message }) => wire.hold(id, head, message),
303
+ "head.release": async ({ session: id, head, message }) => {
304
+ await wire.release(id, head, message);
305
+
306
+ return null;
307
+ },
296
308
 
297
309
  "heads.create": ({ session: id, name, input }) => session(id).heads.create(name, input),
298
310
  "heads.merge": ({ session: id, name, options }) => session(id).heads.merge(name, options),
@@ -347,6 +359,11 @@ export function bindConnection(connection: Connection, options: BindingOptions):
347
359
  ? null
348
360
  : { action: String(answer.action), input: toJson(answer.input) };
349
361
  },
362
+ "part.watch": async ({ session: id, part }) => {
363
+ await session(id).info();
364
+
365
+ return session(id).part(part).watch();
366
+ },
350
367
  "part.close": async ({ session: id, part }) => {
351
368
  await session(id).part(part).close();
352
369
 
@@ -159,7 +159,6 @@ async function* historyLines(path: string, start: number, signal: AbortSignal |
159
159
  const chunk = pending.length === 0 ? read : Buffer.concat([pending, read]);
160
160
  let from = 0;
161
161
 
162
- // oxlint-disable-next-line nyte-code/no-index-of -- a byte search keeps offsets exact, which decoded text cannot
163
162
  for (let newline = chunk.indexOf(0x0a); newline !== -1; newline = chunk.indexOf(0x0a, from)) {
164
163
  yield {
165
164
  text: chunk.toString("utf8", from, newline),