@zenera/cli 1.1.36 → 1.1.37
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/dist/archive.js +1 -1
- package/dist/batch.js +2 -3
- package/dist/cache.js +1 -1
- package/dist/candidates.js +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/commands/cache.js +1 -1
- package/dist/commands/check.js +1 -3
- package/dist/commands/export.js +1 -3
- package/dist/commands/import.js +1 -2
- package/dist/commands/init.js +1 -4
- package/dist/commands/inspect.d.ts +1 -1
- package/dist/commands/inspect.js +1 -4
- package/dist/commands/key.js +1 -2
- package/dist/commands/list.js +4 -5
- package/dist/commands/memory.js +1 -2
- package/dist/commands/meta.js +2 -4
- package/dist/commands/models.d.ts +1 -1
- package/dist/commands/models.js +1 -2
- package/dist/commands/run.js +2 -2
- package/dist/commands/sandbox.js +1 -2
- package/dist/engine.d.ts +3 -90
- package/dist/engine.js +16 -302
- package/dist/history.js +1 -1
- package/dist/lib.d.ts +1 -2
- package/dist/lib.js +1 -2
- package/dist/liveness.d.ts +1 -1
- package/dist/liveness.js +1 -1
- package/dist/meta/host.d.ts +1 -2
- package/dist/meta/host.js +1 -2
- package/dist/modelref.d.ts +1 -1
- package/dist/podman.d.ts +1 -1
- package/dist/podman.js +1 -1
- package/dist/resolve.d.ts +2 -2
- package/dist/resolve.js +2 -3
- package/dist/resources.js +1 -1
- package/dist/sandbox.d.ts +2 -60
- package/dist/sandbox.js +1 -200
- package/dist/term.d.ts +1 -22
- package/dist/term.js +3 -26
- package/dist/tui/app.js +1 -1
- package/dist/tui/models.d.ts +1 -1
- package/dist/validate.d.ts +1 -2
- package/dist/validate.js +1 -3
- package/package.json +4 -3
- package/dist/audit.d.ts +0 -58
- package/dist/audit.js +0 -148
- package/dist/env.d.ts +0 -20
- package/dist/env.js +0 -65
- package/dist/home.d.ts +0 -35
- package/dist/home.js +0 -112
- package/dist/ids.d.ts +0 -12
- package/dist/ids.js +0 -44
- package/dist/image.d.ts +0 -16
- package/dist/image.js +0 -85
- package/dist/keys.d.ts +0 -269
- package/dist/keys.js +0 -624
- package/dist/projects.d.ts +0 -91
- package/dist/projects.js +0 -303
- package/dist/session.d.ts +0 -112
- package/dist/session.js +0 -188
package/dist/projects.d.ts
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { type ProjectConfig, type SandboxMount } from '@zenera/neo';
|
|
2
|
-
/** Whether a directory is a project: whether the loader has something to read. */
|
|
3
|
-
export declare function isProjectDir(dir: string): boolean;
|
|
4
|
-
/** A project resolved on disk. */
|
|
5
|
-
export interface Project {
|
|
6
|
-
dir: string;
|
|
7
|
-
/** what it is called: the registry's name, or the directory's own */
|
|
8
|
-
name: string;
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* The trees a run mounts besides the workspace itself.
|
|
12
|
-
*
|
|
13
|
-
* One array, handed to the file tools and to the container both, because the
|
|
14
|
-
* two have to agree on the name: a path `run_command` prints has to be a path
|
|
15
|
-
* `read_file` takes. Everything here is read-only — material an agent consults
|
|
16
|
-
* and does not edit — and the paths are resolved, because the podman machine on
|
|
17
|
-
* macOS shares the real path or nothing.
|
|
18
|
-
*/
|
|
19
|
-
export declare function projectMounts(root: string, config: ProjectConfig, memoryAt?: string): SandboxMount[];
|
|
20
|
-
/** Walks up from `start` looking for a project configuration. */
|
|
21
|
-
export declare function findUp(start: string): Promise<Project | undefined>;
|
|
22
|
-
export declare function openDir(dir: string): Promise<Project>;
|
|
23
|
-
/**
|
|
24
|
-
* A name from the registry, or a path. A value that looks like either is tried
|
|
25
|
-
* as both, path first, because a directory that exists is unambiguous evidence
|
|
26
|
-
* and a stale registry entry is not.
|
|
27
|
-
*/
|
|
28
|
-
export declare function open(nameOrDir: string): Promise<Project>;
|
|
29
|
-
/**
|
|
30
|
-
* `open` where a miss is an answer rather than an error. It exists for the one
|
|
31
|
-
* place a word might be a project name and might be something else entirely —
|
|
32
|
-
* `zen run <project>` against `zen run <prompt>` — and the difference decides
|
|
33
|
-
* how the rest of the line is read.
|
|
34
|
-
*/
|
|
35
|
-
export declare function find(nameOrDir: string): Promise<Project | undefined>;
|
|
36
|
-
/** The project a bare command means: the one you are standing in. */
|
|
37
|
-
export declare function current(cwd?: string): Promise<Project | undefined>;
|
|
38
|
-
export interface RegistryEntry {
|
|
39
|
-
name: string;
|
|
40
|
-
path: string;
|
|
41
|
-
addedAt: string;
|
|
42
|
-
}
|
|
43
|
-
export declare class Registry {
|
|
44
|
-
#private;
|
|
45
|
-
private constructor();
|
|
46
|
-
static open(): Promise<Registry>;
|
|
47
|
-
get entries(): readonly RegistryEntry[];
|
|
48
|
-
find(name: string): RegistryEntry | undefined;
|
|
49
|
-
findPath(dir: string): RegistryEntry | undefined;
|
|
50
|
-
/** Idempotent: re-registering the same directory refreshes it in place. */
|
|
51
|
-
add(name: string, dir: string): RegistryEntry;
|
|
52
|
-
remove(name: string): boolean;
|
|
53
|
-
/** Drops entries whose directory is gone. Returns what it dropped. */
|
|
54
|
-
prune(): RegistryEntry[];
|
|
55
|
-
save(): void;
|
|
56
|
-
}
|
|
57
|
-
export interface ProjectSummary {
|
|
58
|
-
name: string;
|
|
59
|
-
path: string;
|
|
60
|
-
/** false when the directory has gone away since it was registered */
|
|
61
|
-
present: boolean;
|
|
62
|
-
sessions: number;
|
|
63
|
-
runs: number;
|
|
64
|
-
lastRunAt?: string;
|
|
65
|
-
/** a session currently held by a live process */
|
|
66
|
-
busy: boolean;
|
|
67
|
-
}
|
|
68
|
-
export declare function summarize(entry: RegistryEntry): Promise<ProjectSummary>;
|
|
69
|
-
export declare const sessionsDir: (projectDir: string) => string;
|
|
70
|
-
/** Session ids, oldest first. Only well-formed stamps count as sessions. */
|
|
71
|
-
export declare function sessionIds(projectDir: string): string[];
|
|
72
|
-
export declare function runIds(sessionDir: string): string[];
|
|
73
|
-
/**
|
|
74
|
-
* When this project was last worked in, as a stamp, or `undefined` for one
|
|
75
|
-
* that never has been. Session and run ids are stamps in the same format, so
|
|
76
|
-
* the newest of either compares directly — and reading directory NAMES is what
|
|
77
|
-
* keeps this cheap enough to ask of every registered project before drawing a
|
|
78
|
-
* chooser.
|
|
79
|
-
*/
|
|
80
|
-
export declare function lastUsedAt(projectDir: string): string | undefined;
|
|
81
|
-
/**
|
|
82
|
-
* What a tree occupies, in allocated blocks rather than in bytes, because a
|
|
83
|
-
* sparse file costs what it was given and not what it claims. Symlinks are
|
|
84
|
-
* counted and never followed: a link out of the tree is not part of it, and
|
|
85
|
-
* one that points back in would otherwise be counted twice — or forever.
|
|
86
|
-
*/
|
|
87
|
-
export declare function dirSize(dir: string): number;
|
|
88
|
-
/** True when a session's lock names a process that is still alive. */
|
|
89
|
-
export declare function isBusy(sessionDir: string): boolean;
|
|
90
|
-
export declare function projectName(dir: string): string;
|
|
91
|
-
//# sourceMappingURL=projects.d.ts.map
|
package/dist/projects.js
DELETED
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { ASSETS_MOUNT, assetsDir, FILES_DIR, MEMORY_MOUNT, memoryDir, pidAlive, skillDirs, skillMounts, SKILLS_MOUNT, } from '@zenera/neo';
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
|
-
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
-
import { paths, readJson, writeJson } from "./home.js";
|
|
5
|
-
import { isStamp } from "./ids.js";
|
|
6
|
-
import { invalidError, usageError } from "./term.js";
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
// Projects
|
|
9
|
-
//
|
|
10
|
-
// The directory is the truth, and what makes a directory a project is the one
|
|
11
|
-
// file the runtime cannot do without: `agents.yaml`. There is no marker file
|
|
12
|
-
// beside it — that would be a second thing to keep in sync, holding a name the
|
|
13
|
-
// directory already has.
|
|
14
|
-
//
|
|
15
|
-
// `~/.zenera/neo/projects.json` is an index so that `zen list` and `zen open` do
|
|
16
|
-
// not have to search the filesystem, and it is allowed to be wrong: every entry
|
|
17
|
-
// can be rebuilt by pointing `zen` at the directory again, and an entry whose
|
|
18
|
-
// path has vanished is reported, not fatal.
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
/**
|
|
21
|
-
* The loader's own names, in its own order — `packages/neo/src/project/load.ts`.
|
|
22
|
-
* Anything the library would load, `zen` finds.
|
|
23
|
-
*/
|
|
24
|
-
const CONFIG_NAMES = ['agents.yaml', 'agents.yml', 'agents/agents.yaml', 'agents/agents.yml'];
|
|
25
|
-
/** Whether a directory is a project: whether the loader has something to read. */
|
|
26
|
-
export function isProjectDir(dir) {
|
|
27
|
-
return CONFIG_NAMES.some((name) => existsSync(join(dir, name)));
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* The trees a run mounts besides the workspace itself.
|
|
31
|
-
*
|
|
32
|
-
* One array, handed to the file tools and to the container both, because the
|
|
33
|
-
* two have to agree on the name: a path `run_command` prints has to be a path
|
|
34
|
-
* `read_file` takes. Everything here is read-only — material an agent consults
|
|
35
|
-
* and does not edit — and the paths are resolved, because the podman machine on
|
|
36
|
-
* macOS shares the real path or nothing.
|
|
37
|
-
*/
|
|
38
|
-
export function projectMounts(root, config, memoryAt) {
|
|
39
|
-
const mounts = [];
|
|
40
|
-
const assets = assetsDir(root, config);
|
|
41
|
-
if (assets) {
|
|
42
|
-
mounts.push({ host: realpathSync(assets), at: ASSETS_MOUNT, readOnly: true });
|
|
43
|
-
}
|
|
44
|
-
// The catalog goes in whole, at fixed names, before anything is loaded: a
|
|
45
|
-
// container's mounts are decided when it is created, so a skill folder
|
|
46
|
-
// cannot be added at the moment `skill_load` asks for it.
|
|
47
|
-
for (const dir of skillMounts(skillDirs(root, config), SKILLS_MOUNT)) {
|
|
48
|
-
mounts.push({ host: realpathSync(dir.path), at: dir.at, readOnly: true });
|
|
49
|
-
}
|
|
50
|
-
const memory = memoryDir(root, config, memoryAt);
|
|
51
|
-
if (memory) {
|
|
52
|
-
// Created here because the mount is decided before the project is
|
|
53
|
-
// loaded, and podman would refuse a source that does not exist yet.
|
|
54
|
-
const files = join(memory, FILES_DIR);
|
|
55
|
-
mkdirSync(files, { recursive: true });
|
|
56
|
-
mounts.push({ host: realpathSync(files), at: MEMORY_MOUNT, readOnly: true });
|
|
57
|
-
}
|
|
58
|
-
return mounts;
|
|
59
|
-
}
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// The project directory
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
/**
|
|
64
|
-
* What a project answers to. The registry is asked first, so one registered
|
|
65
|
-
* under a name of its own keeps it; the directory's name answers for a project
|
|
66
|
-
* that was never registered — which is a working project, just not a listed one.
|
|
67
|
-
*/
|
|
68
|
-
async function nameOf(dir) {
|
|
69
|
-
return (await Registry.open()).findPath(dir)?.name ?? basename(dir);
|
|
70
|
-
}
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// Finding one
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
/** Walks up from `start` looking for a project configuration. */
|
|
75
|
-
export async function findUp(start) {
|
|
76
|
-
let dir = resolve(start);
|
|
77
|
-
for (;;) {
|
|
78
|
-
if (isProjectDir(dir)) {
|
|
79
|
-
return { dir, name: await nameOf(dir) };
|
|
80
|
-
}
|
|
81
|
-
const parent = dirname(dir);
|
|
82
|
-
if (parent === dir) {
|
|
83
|
-
return undefined;
|
|
84
|
-
}
|
|
85
|
-
dir = parent;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
export async function openDir(dir) {
|
|
89
|
-
const at = resolve(dir);
|
|
90
|
-
if (!isProjectDir(at)) {
|
|
91
|
-
throw invalidError(`${at} is not a project`, `no ${CONFIG_NAMES[0]} — run: zen init`);
|
|
92
|
-
}
|
|
93
|
-
return { dir: at, name: await nameOf(at) };
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* A name from the registry, or a path. A value that looks like either is tried
|
|
97
|
-
* as both, path first, because a directory that exists is unambiguous evidence
|
|
98
|
-
* and a stale registry entry is not.
|
|
99
|
-
*/
|
|
100
|
-
export async function open(nameOrDir) {
|
|
101
|
-
if (isAbsolute(nameOrDir) || nameOrDir.startsWith('.') || existsSync(nameOrDir)) {
|
|
102
|
-
return openDir(nameOrDir);
|
|
103
|
-
}
|
|
104
|
-
const entry = (await Registry.open()).find(nameOrDir);
|
|
105
|
-
if (!entry) {
|
|
106
|
-
throw usageError(`no project named "${nameOrDir}"`, 'see: zen list');
|
|
107
|
-
}
|
|
108
|
-
return openDir(entry.path);
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* `open` where a miss is an answer rather than an error. It exists for the one
|
|
112
|
-
* place a word might be a project name and might be something else entirely —
|
|
113
|
-
* `zen run <project>` against `zen run <prompt>` — and the difference decides
|
|
114
|
-
* how the rest of the line is read.
|
|
115
|
-
*/
|
|
116
|
-
export async function find(nameOrDir) {
|
|
117
|
-
if (isAbsolute(nameOrDir) || nameOrDir.startsWith('.') || existsSync(nameOrDir)) {
|
|
118
|
-
const dir = resolve(nameOrDir);
|
|
119
|
-
return isProjectDir(dir) ? { dir, name: await nameOf(dir) } : undefined;
|
|
120
|
-
}
|
|
121
|
-
const entry = (await Registry.open()).find(nameOrDir);
|
|
122
|
-
return entry ? openDir(entry.path) : undefined;
|
|
123
|
-
}
|
|
124
|
-
/** The project a bare command means: the one you are standing in. */
|
|
125
|
-
export async function current(cwd = process.cwd()) {
|
|
126
|
-
return findUp(cwd);
|
|
127
|
-
}
|
|
128
|
-
export class Registry {
|
|
129
|
-
#file;
|
|
130
|
-
constructor(file) {
|
|
131
|
-
this.#file = file;
|
|
132
|
-
}
|
|
133
|
-
static async open() {
|
|
134
|
-
const file = await readJson(paths.projects(), {
|
|
135
|
-
version: 1,
|
|
136
|
-
projects: [],
|
|
137
|
-
});
|
|
138
|
-
return new Registry({ version: 1, projects: file.projects ?? [] });
|
|
139
|
-
}
|
|
140
|
-
get entries() {
|
|
141
|
-
return this.#file.projects;
|
|
142
|
-
}
|
|
143
|
-
find(name) {
|
|
144
|
-
return this.#file.projects.find((p) => p.name === name);
|
|
145
|
-
}
|
|
146
|
-
findPath(dir) {
|
|
147
|
-
const at = resolve(dir);
|
|
148
|
-
return this.#file.projects.find((p) => resolve(p.path) === at);
|
|
149
|
-
}
|
|
150
|
-
/** Idempotent: re-registering the same directory refreshes it in place. */
|
|
151
|
-
add(name, dir) {
|
|
152
|
-
const path = resolve(dir);
|
|
153
|
-
const existing = this.find(name);
|
|
154
|
-
if (existing && resolve(existing.path) !== path) {
|
|
155
|
-
throw usageError(`a different project is already named "${name}"`, `it lives at ${existing.path} — use --name`);
|
|
156
|
-
}
|
|
157
|
-
const byPath = this.findPath(path);
|
|
158
|
-
if (byPath) {
|
|
159
|
-
byPath.name = name;
|
|
160
|
-
return byPath;
|
|
161
|
-
}
|
|
162
|
-
const entry = { name, path, addedAt: new Date().toISOString() };
|
|
163
|
-
this.#file.projects.push(entry);
|
|
164
|
-
return entry;
|
|
165
|
-
}
|
|
166
|
-
remove(name) {
|
|
167
|
-
const at = this.#file.projects.findIndex((p) => p.name === name);
|
|
168
|
-
if (at < 0) {
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
this.#file.projects.splice(at, 1);
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
/** Drops entries whose directory is gone. Returns what it dropped. */
|
|
175
|
-
prune() {
|
|
176
|
-
const gone = this.#file.projects.filter((p) => !isProjectDir(p.path));
|
|
177
|
-
this.#file.projects = this.#file.projects.filter((p) => !gone.includes(p));
|
|
178
|
-
return gone;
|
|
179
|
-
}
|
|
180
|
-
save() {
|
|
181
|
-
writeJson(paths.projects(), this.#file, 0o600);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
export async function summarize(entry) {
|
|
185
|
-
const base = {
|
|
186
|
-
name: entry.name,
|
|
187
|
-
path: entry.path,
|
|
188
|
-
present: false,
|
|
189
|
-
sessions: 0,
|
|
190
|
-
runs: 0,
|
|
191
|
-
busy: false,
|
|
192
|
-
};
|
|
193
|
-
if (!isProjectDir(entry.path)) {
|
|
194
|
-
return base;
|
|
195
|
-
}
|
|
196
|
-
const summary = { ...base, present: true };
|
|
197
|
-
for (const session of sessionIds(entry.path)) {
|
|
198
|
-
summary.sessions++;
|
|
199
|
-
const dir = join(entry.path, 'sessions', session);
|
|
200
|
-
if (isBusy(dir)) {
|
|
201
|
-
summary.busy = true;
|
|
202
|
-
}
|
|
203
|
-
const ids = runIds(dir);
|
|
204
|
-
summary.runs += ids.length;
|
|
205
|
-
const newest = ids.at(-1);
|
|
206
|
-
if (newest && (!summary.lastRunAt || newest > summary.lastRunAt)) {
|
|
207
|
-
summary.lastRunAt = newest;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return summary;
|
|
211
|
-
}
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
|
-
// Sessions and runs
|
|
214
|
-
//
|
|
215
|
-
// Kept here rather than in session.ts because listing them is a read-only
|
|
216
|
-
// question about a directory, and `zen list` must be able to ask it without
|
|
217
|
-
// pulling in the runtime.
|
|
218
|
-
// ---------------------------------------------------------------------------
|
|
219
|
-
export const sessionsDir = (projectDir) => join(projectDir, 'sessions');
|
|
220
|
-
/** Session ids, oldest first. Only well-formed stamps count as sessions. */
|
|
221
|
-
export function sessionIds(projectDir) {
|
|
222
|
-
return stampedChildren(sessionsDir(projectDir));
|
|
223
|
-
}
|
|
224
|
-
export function runIds(sessionDir) {
|
|
225
|
-
return stampedChildren(join(sessionDir, 'runs'));
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* When this project was last worked in, as a stamp, or `undefined` for one
|
|
229
|
-
* that never has been. Session and run ids are stamps in the same format, so
|
|
230
|
-
* the newest of either compares directly — and reading directory NAMES is what
|
|
231
|
-
* keeps this cheap enough to ask of every registered project before drawing a
|
|
232
|
-
* chooser.
|
|
233
|
-
*/
|
|
234
|
-
export function lastUsedAt(projectDir) {
|
|
235
|
-
let last;
|
|
236
|
-
for (const id of sessionIds(projectDir)) {
|
|
237
|
-
const newest = runIds(join(sessionsDir(projectDir), id)).at(-1) ?? id;
|
|
238
|
-
if (!last || newest > last) {
|
|
239
|
-
last = newest;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
return last;
|
|
243
|
-
}
|
|
244
|
-
function stampedChildren(dir) {
|
|
245
|
-
if (!existsSync(dir)) {
|
|
246
|
-
return [];
|
|
247
|
-
}
|
|
248
|
-
return readdirSync(dir, { withFileTypes: true })
|
|
249
|
-
.filter((e) => e.isDirectory() && isStamp(e.name))
|
|
250
|
-
.map((e) => e.name)
|
|
251
|
-
.sort();
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* What a tree occupies, in allocated blocks rather than in bytes, because a
|
|
255
|
-
* sparse file costs what it was given and not what it claims. Symlinks are
|
|
256
|
-
* counted and never followed: a link out of the tree is not part of it, and
|
|
257
|
-
* one that points back in would otherwise be counted twice — or forever.
|
|
258
|
-
*/
|
|
259
|
-
export function dirSize(dir) {
|
|
260
|
-
let total = 0;
|
|
261
|
-
const stack = [dir];
|
|
262
|
-
while (stack.length > 0) {
|
|
263
|
-
const at = stack.pop();
|
|
264
|
-
let entries;
|
|
265
|
-
try {
|
|
266
|
-
entries = readdirSync(at, { withFileTypes: true });
|
|
267
|
-
}
|
|
268
|
-
catch {
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
for (const entry of entries) {
|
|
272
|
-
const path = join(at, entry.name);
|
|
273
|
-
if (entry.isDirectory()) {
|
|
274
|
-
stack.push(path);
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
try {
|
|
278
|
-
total += lstatSync(path).blocks * 512;
|
|
279
|
-
}
|
|
280
|
-
catch {
|
|
281
|
-
// Gone between the listing and the stat: a live session is
|
|
282
|
-
// writing, which is normal and not worth failing a report for.
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return total;
|
|
287
|
-
}
|
|
288
|
-
/** True when a session's lock names a process that is still alive. */
|
|
289
|
-
export function isBusy(sessionDir) {
|
|
290
|
-
const path = join(sessionDir, '.lock');
|
|
291
|
-
try {
|
|
292
|
-
const { pid } = JSON.parse(readFileSync(path, 'utf8'));
|
|
293
|
-
return typeof pid === 'number' && pidAlive(pid);
|
|
294
|
-
}
|
|
295
|
-
catch {
|
|
296
|
-
// Absent, unreadable or malformed: nothing is holding it.
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
export function projectName(dir) {
|
|
301
|
-
return basename(resolve(dir));
|
|
302
|
-
}
|
|
303
|
-
//# sourceMappingURL=projects.js.map
|
package/dist/session.d.ts
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
export interface SessionPaths {
|
|
2
|
-
id: string;
|
|
3
|
-
dir: string;
|
|
4
|
-
/** what the agent can see and write */
|
|
5
|
-
workspace: string;
|
|
6
|
-
data: string;
|
|
7
|
-
/** the live, resumable state — rewritten after every run */
|
|
8
|
-
state: string;
|
|
9
|
-
blobs: string;
|
|
10
|
-
runs: string;
|
|
11
|
-
lock: string;
|
|
12
|
-
meta: string;
|
|
13
|
-
}
|
|
14
|
-
export declare function sessionPaths(projectDir: string, id: string): SessionPaths;
|
|
15
|
-
export interface SessionMeta {
|
|
16
|
-
version: 1;
|
|
17
|
-
id: string;
|
|
18
|
-
createdAt: string;
|
|
19
|
-
/**
|
|
20
|
-
* Absolute path the agent's file tools are rooted at. Recorded so resuming
|
|
21
|
-
* never re-asks and never silently moves — a session that quietly changed
|
|
22
|
-
* what "the workspace" meant between turns would be unexplainable.
|
|
23
|
-
*/
|
|
24
|
-
workspace: string;
|
|
25
|
-
lastRunAt?: string;
|
|
26
|
-
title?: string;
|
|
27
|
-
}
|
|
28
|
-
export declare function createSession(projectDir: string, id: string, workspace: string): SessionPaths;
|
|
29
|
-
export declare function readSessionMeta(p: SessionPaths): Promise<SessionMeta>;
|
|
30
|
-
export declare function writeSessionMeta(p: SessionPaths, meta: SessionMeta): void;
|
|
31
|
-
export interface SessionSummary {
|
|
32
|
-
id: string;
|
|
33
|
-
createdAt?: string;
|
|
34
|
-
runs: number;
|
|
35
|
-
lastRunAt?: string;
|
|
36
|
-
busy: boolean;
|
|
37
|
-
title?: string;
|
|
38
|
-
}
|
|
39
|
-
/** Newest first — the order a picker wants. */
|
|
40
|
-
export declare function listSessions(projectDir: string): Promise<SessionSummary[]>;
|
|
41
|
-
export declare function newestSession(projectDir: string): string | undefined;
|
|
42
|
-
export declare function requireSession(projectDir: string, id: string): SessionPaths;
|
|
43
|
-
export interface Held {
|
|
44
|
-
release(): void;
|
|
45
|
-
}
|
|
46
|
-
export declare function acquire(p: SessionPaths): Held;
|
|
47
|
-
export interface RunPaths {
|
|
48
|
-
id: string;
|
|
49
|
-
dir: string;
|
|
50
|
-
input: string;
|
|
51
|
-
output: string;
|
|
52
|
-
state: string;
|
|
53
|
-
report: string;
|
|
54
|
-
/** the same run as one Mermaid flowchart, for a model to read */
|
|
55
|
-
graph: string;
|
|
56
|
-
meta: string;
|
|
57
|
-
}
|
|
58
|
-
export declare function runPaths(session: SessionPaths, id: string): RunPaths;
|
|
59
|
-
export declare function createRun(session: SessionPaths): RunPaths;
|
|
60
|
-
/**
|
|
61
|
-
* The same paths, reached from a directory instead of from a pair of ids.
|
|
62
|
-
*
|
|
63
|
-
* `zen run --json` hands back a run directory, and a caller holding one has no
|
|
64
|
-
* reason to take it apart again to ask a second question about the same run.
|
|
65
|
-
* The layout is fixed, so the ids can be read back out of the path — and both
|
|
66
|
-
* still go through `isStamp`, so a path from anywhere cannot name a directory
|
|
67
|
-
* this layout would never have produced.
|
|
68
|
-
*
|
|
69
|
-
* The session comes back too, not only the run: blobs are stored per session,
|
|
70
|
-
* so anything that resolves a payload needs both halves — and the project, so
|
|
71
|
-
* a command reached this way can still read the project's config.
|
|
72
|
-
*/
|
|
73
|
-
export declare function runPathsAt(dir: string): {
|
|
74
|
-
project: string;
|
|
75
|
-
session: SessionPaths;
|
|
76
|
-
run: RunPaths;
|
|
77
|
-
};
|
|
78
|
-
export interface RunMeta {
|
|
79
|
-
version: 1;
|
|
80
|
-
id: string;
|
|
81
|
-
session: string;
|
|
82
|
-
startedAt: string;
|
|
83
|
-
finishedAt: string;
|
|
84
|
-
durationMs: number;
|
|
85
|
-
agent: string;
|
|
86
|
-
stopReason: string;
|
|
87
|
-
turns: number;
|
|
88
|
-
usage: unknown;
|
|
89
|
-
workspace: string;
|
|
90
|
-
memory?: string;
|
|
91
|
-
error?: string;
|
|
92
|
-
}
|
|
93
|
-
export declare function writeRunMeta(p: RunPaths, meta: RunMeta): void;
|
|
94
|
-
/**
|
|
95
|
-
* Partial, because a run killed before it recorded anything still has a
|
|
96
|
-
* directory, and every field here is worth having on its own.
|
|
97
|
-
*/
|
|
98
|
-
export declare function readRunMeta(p: RunPaths): Promise<Partial<RunMeta>>;
|
|
99
|
-
export interface RunSummary {
|
|
100
|
-
id: string;
|
|
101
|
-
startedAt?: string;
|
|
102
|
-
finishedAt?: string;
|
|
103
|
-
agent?: string;
|
|
104
|
-
stopReason?: string;
|
|
105
|
-
error?: string;
|
|
106
|
-
}
|
|
107
|
-
/** Newest first — the order a picker wants. */
|
|
108
|
-
export declare function listRuns(session: SessionPaths): Promise<RunSummary[]>;
|
|
109
|
-
export declare function newestRun(session: SessionPaths): string | undefined;
|
|
110
|
-
/** A path to show a human: relative when that is shorter, absolute otherwise. */
|
|
111
|
-
export declare function display(path: string, from?: string): string;
|
|
112
|
-
//# sourceMappingURL=session.d.ts.map
|
package/dist/session.js
DELETED
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
import { claimLock, ownLock } from '@zenera/neo';
|
|
2
|
-
import { existsSync, mkdirSync, rmSync } from 'node:fs';
|
|
3
|
-
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
4
|
-
import { readJson, writeJson } from "./home.js";
|
|
5
|
-
import { isStamp, stamp, stampInstant } from "./ids.js";
|
|
6
|
-
import { isBusy, runIds, sessionIds, sessionsDir } from "./projects.js";
|
|
7
|
-
import { CliError, EXIT, invalidError, usageError } from "./term.js";
|
|
8
|
-
export function sessionPaths(projectDir, id) {
|
|
9
|
-
if (!isStamp(id)) {
|
|
10
|
-
throw usageError(`"${id}" is not a session id`, 'ids look like 20260825-143012-a7f3');
|
|
11
|
-
}
|
|
12
|
-
const dir = join(sessionsDir(projectDir), id);
|
|
13
|
-
const data = join(dir, '.data');
|
|
14
|
-
return {
|
|
15
|
-
id,
|
|
16
|
-
dir,
|
|
17
|
-
workspace: join(dir, 'workspace'),
|
|
18
|
-
data,
|
|
19
|
-
state: join(data, 'state.json'),
|
|
20
|
-
blobs: join(data, 'blobs'),
|
|
21
|
-
runs: join(dir, 'runs'),
|
|
22
|
-
lock: join(dir, '.lock'),
|
|
23
|
-
meta: join(data, 'session.json'),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Creating and finding
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
export function createSession(projectDir, id, workspace) {
|
|
30
|
-
const p = sessionPaths(projectDir, id);
|
|
31
|
-
mkdirSync(p.data, { recursive: true });
|
|
32
|
-
mkdirSync(p.runs, { recursive: true });
|
|
33
|
-
mkdirSync(resolve(workspace), { recursive: true });
|
|
34
|
-
const meta = {
|
|
35
|
-
version: 1,
|
|
36
|
-
id: p.id,
|
|
37
|
-
createdAt: new Date().toISOString(),
|
|
38
|
-
workspace: resolve(workspace),
|
|
39
|
-
};
|
|
40
|
-
writeJson(p.meta, meta, 0o644);
|
|
41
|
-
return p;
|
|
42
|
-
}
|
|
43
|
-
export async function readSessionMeta(p) {
|
|
44
|
-
const meta = await readJson(p.meta, {});
|
|
45
|
-
return {
|
|
46
|
-
version: 1,
|
|
47
|
-
id: p.id,
|
|
48
|
-
createdAt: meta.createdAt ?? new Date().toISOString(),
|
|
49
|
-
// Sessions from before the workspace was recorded fall back to their
|
|
50
|
-
// own directory, which is where it would have been.
|
|
51
|
-
workspace: meta.workspace ?? p.workspace,
|
|
52
|
-
lastRunAt: meta.lastRunAt,
|
|
53
|
-
title: meta.title,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
export function writeSessionMeta(p, meta) {
|
|
57
|
-
writeJson(p.meta, meta, 0o644);
|
|
58
|
-
}
|
|
59
|
-
/** Newest first — the order a picker wants. */
|
|
60
|
-
export async function listSessions(projectDir) {
|
|
61
|
-
const out = [];
|
|
62
|
-
for (const id of sessionIds(projectDir).reverse()) {
|
|
63
|
-
const p = sessionPaths(projectDir, id);
|
|
64
|
-
const meta = await readSessionMeta(p);
|
|
65
|
-
const ids = runIds(p.dir);
|
|
66
|
-
out.push({
|
|
67
|
-
id,
|
|
68
|
-
createdAt: meta.createdAt,
|
|
69
|
-
runs: ids.length,
|
|
70
|
-
lastRunAt: meta.lastRunAt,
|
|
71
|
-
busy: isBusy(p.dir),
|
|
72
|
-
title: meta.title,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
return out;
|
|
76
|
-
}
|
|
77
|
-
export function newestSession(projectDir) {
|
|
78
|
-
return sessionIds(projectDir).at(-1);
|
|
79
|
-
}
|
|
80
|
-
export function requireSession(projectDir, id) {
|
|
81
|
-
const p = sessionPaths(projectDir, id);
|
|
82
|
-
if (!existsSync(p.dir)) {
|
|
83
|
-
throw invalidError(`no session ${id}`, 'see: zen list --sessions');
|
|
84
|
-
}
|
|
85
|
-
return p;
|
|
86
|
-
}
|
|
87
|
-
export function acquire(p) {
|
|
88
|
-
claimLock(p.lock, ownLock(), (held) => new CliError(`session ${p.id} is already running (pid ${held.pid})`, EXIT.failed, 'wait for it, or start another with --new'), 2);
|
|
89
|
-
let released = false;
|
|
90
|
-
const release = () => {
|
|
91
|
-
if (released) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
released = true;
|
|
95
|
-
rmSync(p.lock, { force: true });
|
|
96
|
-
};
|
|
97
|
-
return { release };
|
|
98
|
-
}
|
|
99
|
-
export function runPaths(session, id) {
|
|
100
|
-
if (!isStamp(id)) {
|
|
101
|
-
throw usageError(`"${id}" is not a run id`);
|
|
102
|
-
}
|
|
103
|
-
const dir = join(session.runs, id);
|
|
104
|
-
return {
|
|
105
|
-
id,
|
|
106
|
-
dir,
|
|
107
|
-
input: join(dir, 'input.md'),
|
|
108
|
-
output: join(dir, 'output.md'),
|
|
109
|
-
state: join(dir, 'state.json'),
|
|
110
|
-
report: join(dir, 'report.html'),
|
|
111
|
-
graph: join(dir, 'graph.mmd'),
|
|
112
|
-
meta: join(dir, 'meta.json'),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
export function createRun(session) {
|
|
116
|
-
const p = runPaths(session, stamp());
|
|
117
|
-
mkdirSync(p.dir, { recursive: true });
|
|
118
|
-
return p;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* The same paths, reached from a directory instead of from a pair of ids.
|
|
122
|
-
*
|
|
123
|
-
* `zen run --json` hands back a run directory, and a caller holding one has no
|
|
124
|
-
* reason to take it apart again to ask a second question about the same run.
|
|
125
|
-
* The layout is fixed, so the ids can be read back out of the path — and both
|
|
126
|
-
* still go through `isStamp`, so a path from anywhere cannot name a directory
|
|
127
|
-
* this layout would never have produced.
|
|
128
|
-
*
|
|
129
|
-
* The session comes back too, not only the run: blobs are stored per session,
|
|
130
|
-
* so anything that resolves a payload needs both halves — and the project, so
|
|
131
|
-
* a command reached this way can still read the project's config.
|
|
132
|
-
*/
|
|
133
|
-
export function runPathsAt(dir) {
|
|
134
|
-
const full = resolve(dir);
|
|
135
|
-
const runId = basename(full);
|
|
136
|
-
const sessionDir = dirname(dirname(full));
|
|
137
|
-
const wrong = () => usageError(`${display(full)} is not a run directory`, 'runs live at <project>/sessions/<session>/runs/<run>');
|
|
138
|
-
if (!isStamp(runId) || !isStamp(basename(sessionDir)) || basename(dirname(full)) !== 'runs') {
|
|
139
|
-
throw wrong();
|
|
140
|
-
}
|
|
141
|
-
const project = dirname(dirname(sessionDir));
|
|
142
|
-
const session = sessionPaths(project, basename(sessionDir));
|
|
143
|
-
const run = runPaths(session, runId);
|
|
144
|
-
if (resolve(run.dir) !== full) {
|
|
145
|
-
throw wrong();
|
|
146
|
-
}
|
|
147
|
-
if (!existsSync(run.state)) {
|
|
148
|
-
throw invalidError(`run ${runId} has no state.json`, 'only a run that got far enough to save state can be inspected');
|
|
149
|
-
}
|
|
150
|
-
return { project, session, run };
|
|
151
|
-
}
|
|
152
|
-
export function writeRunMeta(p, meta) {
|
|
153
|
-
writeJson(p.meta, meta, 0o644);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Partial, because a run killed before it recorded anything still has a
|
|
157
|
-
* directory, and every field here is worth having on its own.
|
|
158
|
-
*/
|
|
159
|
-
export async function readRunMeta(p) {
|
|
160
|
-
return await readJson(p.meta, {});
|
|
161
|
-
}
|
|
162
|
-
/** Newest first — the order a picker wants. */
|
|
163
|
-
export async function listRuns(session) {
|
|
164
|
-
const out = [];
|
|
165
|
-
for (const id of runIds(session.dir).reverse()) {
|
|
166
|
-
// A run killed before it recorded anything still has an id, and the id
|
|
167
|
-
// says when it started, so it is listable without its meta.
|
|
168
|
-
const meta = await readRunMeta(runPaths(session, id));
|
|
169
|
-
out.push({
|
|
170
|
-
id,
|
|
171
|
-
startedAt: meta.startedAt ?? stampInstant(id),
|
|
172
|
-
finishedAt: meta.finishedAt,
|
|
173
|
-
agent: meta.agent,
|
|
174
|
-
stopReason: meta.stopReason,
|
|
175
|
-
error: meta.error,
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
return out;
|
|
179
|
-
}
|
|
180
|
-
export function newestRun(session) {
|
|
181
|
-
return runIds(session.dir).at(-1);
|
|
182
|
-
}
|
|
183
|
-
/** A path to show a human: relative when that is shorter, absolute otherwise. */
|
|
184
|
-
export function display(path, from = process.cwd()) {
|
|
185
|
-
const rel = relative(from, path);
|
|
186
|
-
return !rel.startsWith('..') && rel.length < path.length ? rel || '.' : path;
|
|
187
|
-
}
|
|
188
|
-
//# sourceMappingURL=session.js.map
|