@ahpd/server 0.1.0
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/LICENSE +21 -0
- package/README.md +129 -0
- package/dist/automations.d.ts +4 -0
- package/dist/automations.d.ts.map +1 -0
- package/dist/automations.js +191 -0
- package/dist/automations.js.map +1 -0
- package/dist/catalog.d.ts +48 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +51 -0
- package/dist/catalog.js.map +1 -0
- package/dist/changes.d.ts +4 -0
- package/dist/changes.d.ts.map +1 -0
- package/dist/changes.js +601 -0
- package/dist/changes.js.map +1 -0
- package/dist/cron.d.ts +43 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +211 -0
- package/dist/cron.js.map +1 -0
- package/dist/git.d.ts +16 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +126 -0
- package/dist/git.js.map +1 -0
- package/dist/host.d.ts +24 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +5850 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/index.js.map +1 -0
- package/dist/listen.d.ts +3 -0
- package/dist/listen.d.ts.map +1 -0
- package/dist/listen.js +183 -0
- package/dist/listen.js.map +1 -0
- package/dist/paging.d.ts +38 -0
- package/dist/paging.d.ts.map +1 -0
- package/dist/paging.js +53 -0
- package/dist/paging.js.map +1 -0
- package/dist/paths.d.ts +15 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +19 -0
- package/dist/paths.js.map +1 -0
- package/dist/resources.d.ts +122 -0
- package/dist/resources.d.ts.map +1 -0
- package/dist/resources.js +494 -0
- package/dist/resources.js.map +1 -0
- package/dist/rpc.d.ts +63 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +208 -0
- package/dist/rpc.js.map +1 -0
- package/dist/scheduled.d.ts +48 -0
- package/dist/scheduled.d.ts.map +1 -0
- package/dist/scheduled.js +325 -0
- package/dist/scheduled.js.map +1 -0
- package/dist/terminals.d.ts +29 -0
- package/dist/terminals.d.ts.map +1 -0
- package/dist/terminals.js +346 -0
- package/dist/terminals.js.map +1 -0
- package/dist/tools.d.ts +18 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +65 -0
- package/dist/tools.js.map +1 -0
- package/dist/types/agent.d.ts +245 -0
- package/dist/types/agent.d.ts.map +1 -0
- package/dist/types/agent.js +3 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/automations.d.ts +164 -0
- package/dist/types/automations.d.ts.map +1 -0
- package/dist/types/automations.js +3 -0
- package/dist/types/automations.js.map +1 -0
- package/dist/types/catalog.d.ts +30 -0
- package/dist/types/catalog.d.ts.map +1 -0
- package/dist/types/catalog.js +3 -0
- package/dist/types/catalog.js.map +1 -0
- package/dist/types/changes.d.ts +272 -0
- package/dist/types/changes.d.ts.map +1 -0
- package/dist/types/changes.js +3 -0
- package/dist/types/changes.js.map +1 -0
- package/dist/types/common.d.ts +9 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/common.js +3 -0
- package/dist/types/common.js.map +1 -0
- package/dist/types/host.d.ts +372 -0
- package/dist/types/host.d.ts.map +1 -0
- package/dist/types/host.js +3 -0
- package/dist/types/host.js.map +1 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/listen.d.ts +48 -0
- package/dist/types/listen.d.ts.map +1 -0
- package/dist/types/listen.js +3 -0
- package/dist/types/listen.js.map +1 -0
- package/dist/types/paging.d.ts +10 -0
- package/dist/types/paging.d.ts.map +1 -0
- package/dist/types/paging.js +3 -0
- package/dist/types/paging.js.map +1 -0
- package/dist/types/probe.d.ts +46 -0
- package/dist/types/probe.d.ts.map +1 -0
- package/dist/types/probe.js +3 -0
- package/dist/types/probe.js.map +1 -0
- package/dist/types/resources.d.ts +83 -0
- package/dist/types/resources.d.ts.map +1 -0
- package/dist/types/resources.js +3 -0
- package/dist/types/resources.js.map +1 -0
- package/dist/types/rpc.d.ts +65 -0
- package/dist/types/rpc.d.ts.map +1 -0
- package/dist/types/rpc.js +3 -0
- package/dist/types/rpc.js.map +1 -0
- package/dist/types/session.d.ts +395 -0
- package/dist/types/session.d.ts.map +1 -0
- package/dist/types/session.js +3 -0
- package/dist/types/session.js.map +1 -0
- package/dist/types/terminals.d.ts +131 -0
- package/dist/types/terminals.d.ts.map +1 -0
- package/dist/types/terminals.js +3 -0
- package/dist/types/terminals.js.map +1 -0
- package/dist/types/wire.d.ts +62 -0
- package/dist/types/wire.d.ts.map +1 -0
- package/dist/types/wire.js +13 -0
- package/dist/types/wire.js.map +1 -0
- package/dist/types/worktrees.d.ts +86 -0
- package/dist/types/worktrees.d.ts.map +1 -0
- package/dist/types/worktrees.js +3 -0
- package/dist/types/worktrees.js.map +1 -0
- package/dist/worktrees.d.ts +27 -0
- package/dist/worktrees.d.ts.map +1 -0
- package/dist/worktrees.js +152 -0
- package/dist/worktrees.js.map +1 -0
- package/package.json +61 -0
- package/src/automations.ts +204 -0
- package/src/catalog.ts +53 -0
- package/src/changes.ts +629 -0
- package/src/cron.ts +251 -0
- package/src/git.ts +132 -0
- package/src/host.ts +5947 -0
- package/src/index.ts +40 -0
- package/src/listen.ts +240 -0
- package/src/paging.ts +56 -0
- package/src/paths.ts +19 -0
- package/src/resources.ts +513 -0
- package/src/rpc.ts +222 -0
- package/src/scheduled.ts +369 -0
- package/src/terminals.ts +353 -0
- package/src/tools.ts +64 -0
- package/src/types/agent.ts +255 -0
- package/src/types/automations.ts +164 -0
- package/src/types/catalog.ts +26 -0
- package/src/types/changes.ts +266 -0
- package/src/types/common.ts +9 -0
- package/src/types/host.ts +386 -0
- package/src/types/index.ts +27 -0
- package/src/types/listen.ts +53 -0
- package/src/types/paging.ts +11 -0
- package/src/types/probe.ts +42 -0
- package/src/types/resources.ts +107 -0
- package/src/types/rpc.ts +68 -0
- package/src/types/session.ts +415 -0
- package/src/types/terminals.ts +133 -0
- package/src/types/wire.ts +69 -0
- package/src/types/worktrees.ts +91 -0
- package/src/worktrees.ts +162 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** A working tree of its own, so two sessions in one repository do not collide. */
|
|
2
|
+
/** What to make, and from where. */
|
|
3
|
+
export interface Worktree {
|
|
4
|
+
/** The repository to make it from. A path, not a URI. */
|
|
5
|
+
repository: string;
|
|
6
|
+
/** The branch to start from. */
|
|
7
|
+
base: string;
|
|
8
|
+
/**
|
|
9
|
+
* The branch to create and check out, or nothing to check out `base` itself.
|
|
10
|
+
*
|
|
11
|
+
* Absent is what `worktreeCreateNewBranch: false` means: the session
|
|
12
|
+
* continues an existing branch rather than starting one. Git refuses a
|
|
13
|
+
* second worktree on a branch already checked out somewhere, which is the
|
|
14
|
+
* right refusal - two sessions on one branch is the collision worktrees
|
|
15
|
+
* exist to prevent.
|
|
16
|
+
*/
|
|
17
|
+
branch?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the created branch tracks the upstream of the one it started from.
|
|
20
|
+
*
|
|
21
|
+
* Off unless asked. A tracking branch pushes at its upstream by default, so
|
|
22
|
+
* a `git push` inside a session's worktree would go at the branch it was
|
|
23
|
+
* started *from* - which is the one mistake in here that reaches a shared
|
|
24
|
+
* repository.
|
|
25
|
+
*/
|
|
26
|
+
track?: boolean;
|
|
27
|
+
/** Where to put it. */
|
|
28
|
+
path: string;
|
|
29
|
+
/**
|
|
30
|
+
* Git-ignored files to copy in, as glob patterns relative to the repository.
|
|
31
|
+
*
|
|
32
|
+
* A checkout carries what git tracks and nothing else, so a worktree of an
|
|
33
|
+
* ordinary project has no `.env`, no `node_modules`, and none of the local
|
|
34
|
+
* configuration the thing needs to run. Without these the isolation works
|
|
35
|
+
* and the session inside it cannot build - which is a failure the person
|
|
36
|
+
* meets after choosing it rather than while choosing it.
|
|
37
|
+
*/
|
|
38
|
+
include?: string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Making and unmaking git worktrees.
|
|
42
|
+
*
|
|
43
|
+
* A port for the reason `DirectoryFacts` and `ChangesetSource` are ports: it
|
|
44
|
+
* spawns `git`, which is a binary rather than a runtime API and may not be
|
|
45
|
+
* installed. A host given none advertises no `isolation` at all, which is the
|
|
46
|
+
* honest answer - a client draws the control a host says it has.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately not part of `ChangesetSource`. That port describes changes; a
|
|
49
|
+
* worktree is a directory, and the two happen to share an implementation
|
|
50
|
+
* rather than a subject.
|
|
51
|
+
*/
|
|
52
|
+
export interface Worktrees {
|
|
53
|
+
/**
|
|
54
|
+
* The repository root above this directory, if it is in one.
|
|
55
|
+
*
|
|
56
|
+
* The root and not the directory: a worktree is made from the repository,
|
|
57
|
+
* and a session pointed at `src/` inside one is still a session in it.
|
|
58
|
+
* Undefined means there is no repository here and no isolation to offer.
|
|
59
|
+
*/
|
|
60
|
+
repository(dir: string): Promise<string | undefined>;
|
|
61
|
+
/**
|
|
62
|
+
* The branches this repository has, most useful first.
|
|
63
|
+
*
|
|
64
|
+
* What a client draws the base-branch picker from. An empty list is a real
|
|
65
|
+
* answer - a repository with no commits has no branches - and leaves the
|
|
66
|
+
* control with nothing to choose but the default.
|
|
67
|
+
*/
|
|
68
|
+
branches(repository: string): Promise<string[]>;
|
|
69
|
+
/** Make one. Answers the path it made, which is where the session runs. */
|
|
70
|
+
create(worktree: Worktree): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* Whether anything in it is uncommitted.
|
|
73
|
+
*
|
|
74
|
+
* Asked before it is removed, and the answer decides whether it is: work
|
|
75
|
+
* nobody committed is work this daemon cannot judge the value of.
|
|
76
|
+
*/
|
|
77
|
+
dirty(path: string): Promise<boolean>;
|
|
78
|
+
/**
|
|
79
|
+
* Take one away, and the branch with it if nothing else points at it.
|
|
80
|
+
*
|
|
81
|
+
* Only ever called for a worktree this host made and only when `dirty`
|
|
82
|
+
* answered no.
|
|
83
|
+
*/
|
|
84
|
+
remove(repository: string, path: string, branch?: string): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
//# sourceMappingURL=worktrees.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../../src/types/worktrees.ts"],"names":[],"mappings":"AAAA,mFAAmF;AAEnF,oCAAoC;AACpC,MAAM,WAAW,QAAQ;IACvB,yDAAyD;IACzD,UAAU,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,uBAAuB;IACvB,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,SAAS;IACxB;;;;;;OAMG;IACH,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;IAErD;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAEhD,2EAA2E;IAC3E,MAAM,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE1C;;;;;OAKG;IACH,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEtC;;;;;OAKG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktrees.js","sourceRoot":"","sources":["../../src/types/worktrees.ts"],"names":[],"mappings":"AAAA,mFAAmF"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Git worktrees, so two sessions in one repository do not edit under each other. */
|
|
2
|
+
import type { Worktrees } from './types/worktrees.js';
|
|
3
|
+
/**
|
|
4
|
+
* Where a repository's worktrees live: `<repo>.worktrees`, beside it.
|
|
5
|
+
*
|
|
6
|
+
* Beside rather than inside, because a worktree inside its own repository is
|
|
7
|
+
* a directory git then has to be told to ignore, and every tool that walks the
|
|
8
|
+
* tree finds the same project twice. The name is the one VS Code's host uses,
|
|
9
|
+
* so a worktree either of them makes is one the other finds.
|
|
10
|
+
*/
|
|
11
|
+
export declare const worktreesOf: (repository: string) => string;
|
|
12
|
+
/** The directory name for a branch: no slashes, since it has to be one segment. */
|
|
13
|
+
export declare const worktreeFor: (branch: string) => string;
|
|
14
|
+
/**
|
|
15
|
+
* Git worktrees, as a host's `Worktrees` port.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately not part of `createHost`: it spawns `git`, which is a binary
|
|
18
|
+
* rather than a runtime API and may not be installed at all. A host without it
|
|
19
|
+
* offers no isolation and every session runs in the folder it was pointed at,
|
|
20
|
+
* which is what this daemon did before there was a choice.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* createHost({ path, agents: [claude({ paths })], worktrees: gitWorktrees() });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function gitWorktrees(): Worktrees;
|
|
27
|
+
//# sourceMappingURL=worktrees.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktrees.d.ts","sourceRoot":"","sources":["../src/worktrees.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAKrF,OAAO,KAAK,EAAY,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAchE;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,GAAI,YAAY,MAAM,KAAG,MACe,CAAC;AAEjE,mFAAmF;AACnF,eAAO,MAAM,WAAW,GAAI,QAAQ,MAAM,KAAG,MACQ,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAmFxC"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/** Git worktrees, so two sessions in one repository do not edit under each other. */
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
|
+
import { cp, mkdir, readdir, stat } from 'node:fs/promises';
|
|
5
|
+
/** Run git, and answer what it said. Rejects with git's own words. */
|
|
6
|
+
const git = (dir, args, ms = 60_000) => new Promise((resolve, reject) => {
|
|
7
|
+
execFile('git', ['-C', dir, ...args], { timeout: ms, maxBuffer: 8 << 20 }, (error, out, bad) => {
|
|
8
|
+
if (!error)
|
|
9
|
+
return resolve(out.toString());
|
|
10
|
+
// git's own message, not node's: "fatal: a branch named x already
|
|
11
|
+
// exists" is something a person can act on and `Command failed` is not.
|
|
12
|
+
const said = bad.toString().trim() || error.message;
|
|
13
|
+
reject(new Error(said));
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Where a repository's worktrees live: `<repo>.worktrees`, beside it.
|
|
18
|
+
*
|
|
19
|
+
* Beside rather than inside, because a worktree inside its own repository is
|
|
20
|
+
* a directory git then has to be told to ignore, and every tool that walks the
|
|
21
|
+
* tree finds the same project twice. The name is the one VS Code's host uses,
|
|
22
|
+
* so a worktree either of them makes is one the other finds.
|
|
23
|
+
*/
|
|
24
|
+
export const worktreesOf = (repository) => join(dirname(repository), `${basename(repository)}.worktrees`);
|
|
25
|
+
/** The directory name for a branch: no slashes, since it has to be one segment. */
|
|
26
|
+
export const worktreeFor = (branch) => branch.replace(/^agents\//, '').replace(/\//g, '-');
|
|
27
|
+
/**
|
|
28
|
+
* Git worktrees, as a host's `Worktrees` port.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately not part of `createHost`: it spawns `git`, which is a binary
|
|
31
|
+
* rather than a runtime API and may not be installed at all. A host without it
|
|
32
|
+
* offers no isolation and every session runs in the folder it was pointed at,
|
|
33
|
+
* which is what this daemon did before there was a choice.
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* createHost({ path, agents: [claude({ paths })], worktrees: gitWorktrees() });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function gitWorktrees() {
|
|
40
|
+
return {
|
|
41
|
+
repository: async (dir) => {
|
|
42
|
+
const found = await git(dir, ['rev-parse', '--show-toplevel'], 5_000).catch(() => undefined);
|
|
43
|
+
const root = found?.trim();
|
|
44
|
+
return root === undefined || root === '' ? undefined : root;
|
|
45
|
+
},
|
|
46
|
+
branches: async (repository) => {
|
|
47
|
+
/*
|
|
48
|
+
* Local branches by most recent commit, then the remote ones.
|
|
49
|
+
*
|
|
50
|
+
* Ordered rather than alphabetical because the useful answer is almost
|
|
51
|
+
* always near the top of the first list, and a picker that opens on
|
|
52
|
+
* `archive/2019-cleanup` is one somebody has to search.
|
|
53
|
+
*/
|
|
54
|
+
const said = await git(repository, ['for-each-ref', '--sort=-committerdate', '--format=%(refname:short)', 'refs/heads', 'refs/remotes'], 10_000).catch(() => '');
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
for (const line of said.split('\n')) {
|
|
57
|
+
const name = line.trim();
|
|
58
|
+
// `origin/HEAD` is a symbolic ref rather than a branch, and checking
|
|
59
|
+
// it out is a detached head instead of the branch somebody meant.
|
|
60
|
+
if (name === '' || name.endsWith('/HEAD'))
|
|
61
|
+
continue;
|
|
62
|
+
seen.add(name.replace(/^origin\//, ''));
|
|
63
|
+
}
|
|
64
|
+
return [...seen];
|
|
65
|
+
},
|
|
66
|
+
create: async (worktree) => {
|
|
67
|
+
await mkdir(dirname(worktree.path), { recursive: true });
|
|
68
|
+
/*
|
|
69
|
+
* A new branch, or the one that was chosen.
|
|
70
|
+
*
|
|
71
|
+
* `--no-track` unless asked, because the new branch is this session's
|
|
72
|
+
* own: tracking would make it push to the base branch's upstream by
|
|
73
|
+
* default, so a `git push` inside a worktree session would go at the
|
|
74
|
+
* branch it was started *from*. That is the one mistake in here that
|
|
75
|
+
* reaches a shared repository.
|
|
76
|
+
*
|
|
77
|
+
* No branch at all is `worktreeCreateNewBranch: false` - the session
|
|
78
|
+
* continues `base` rather than starting something. Git refuses a second
|
|
79
|
+
* worktree on a branch already checked out, and that refusal is right.
|
|
80
|
+
*/
|
|
81
|
+
await git(worktree.repository, worktree.branch === undefined
|
|
82
|
+
? ['worktree', 'add', worktree.path, worktree.base]
|
|
83
|
+
: [
|
|
84
|
+
'worktree', 'add', worktree.track === true ? '--track' : '--no-track',
|
|
85
|
+
'-b', worktree.branch, worktree.path, worktree.base,
|
|
86
|
+
]);
|
|
87
|
+
for (const pattern of worktree.include ?? []) {
|
|
88
|
+
// Best effort, one pattern at a time: a `.env` that is not there is
|
|
89
|
+
// the ordinary case, and a session that refused to start over a
|
|
90
|
+
// missing optional file would be worse than one without it.
|
|
91
|
+
await copy(worktree.repository, worktree.path, pattern).catch(() => undefined);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
dirty: async (path) => {
|
|
95
|
+
// `--porcelain` says nothing at all about a clean tree, which makes the
|
|
96
|
+
// empty answer the reliable one. Untracked files count: a file the
|
|
97
|
+
// agent wrote and never added is exactly the work worth keeping.
|
|
98
|
+
const said = await git(path, ['status', '--porcelain'], 10_000).catch(() => 'unknown');
|
|
99
|
+
return said.trim() !== '';
|
|
100
|
+
},
|
|
101
|
+
remove: async (repository, path, branch) => {
|
|
102
|
+
await git(repository, ['worktree', 'remove', path]);
|
|
103
|
+
/*
|
|
104
|
+
* The branch too, and only the one this host made.
|
|
105
|
+
*
|
|
106
|
+
* Named by the caller rather than derived from the directory: a branch
|
|
107
|
+
* prefix a client asked for changes the name, and guessing it wrong
|
|
108
|
+
* either deletes nothing or - far worse - names somebody else's. `-d`
|
|
109
|
+
* refuses to delete a branch carrying commits nothing else has, which
|
|
110
|
+
* is the same judgement the dirty check makes about uncommitted work.
|
|
111
|
+
*/
|
|
112
|
+
if (branch !== undefined)
|
|
113
|
+
await git(repository, ['branch', '-d', branch]).catch(() => undefined);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Copy one git-ignored path into the new worktree, keeping its place.
|
|
119
|
+
*
|
|
120
|
+
* A glob only in the shallow sense the reference uses it: a literal path, or
|
|
121
|
+
* one trailing `*` in the last segment. A full matcher here would be a
|
|
122
|
+
* dependency and a surprise - the patterns clients send are `.env`,
|
|
123
|
+
* `.env.local` and `node_modules`.
|
|
124
|
+
*/
|
|
125
|
+
const copy = async (from, to, pattern) => {
|
|
126
|
+
if (pattern.includes('..') || pattern.startsWith('/'))
|
|
127
|
+
return;
|
|
128
|
+
const star = pattern.lastIndexOf('*');
|
|
129
|
+
if (star === -1) {
|
|
130
|
+
await cp(join(from, pattern), join(to, pattern), { recursive: true, errorOnExist: false });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const at = pattern.lastIndexOf('/');
|
|
134
|
+
const dir = at === -1 ? '' : pattern.slice(0, at);
|
|
135
|
+
const leaf = at === -1 ? pattern : pattern.slice(at + 1);
|
|
136
|
+
if (leaf.indexOf('*') !== leaf.lastIndexOf('*'))
|
|
137
|
+
return;
|
|
138
|
+
const [before, after] = leaf.split('*');
|
|
139
|
+
const entries = await readdir(join(from, dir)).catch(() => []);
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (!entry.startsWith(before ?? '') || !entry.endsWith(after ?? ''))
|
|
142
|
+
continue;
|
|
143
|
+
const source = join(from, dir, entry);
|
|
144
|
+
// Directories as well as files: `node_modules` is the pattern that makes
|
|
145
|
+
// the difference between a worktree that builds and one that does not.
|
|
146
|
+
const what = await stat(source).catch(() => undefined);
|
|
147
|
+
if (!what)
|
|
148
|
+
continue;
|
|
149
|
+
await cp(source, join(to, dir, entry), { recursive: what.isDirectory(), errorOnExist: false });
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
//# sourceMappingURL=worktrees.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktrees.js","sourceRoot":"","sources":["../src/worktrees.ts"],"names":[],"mappings":"AAAA,qFAAqF;AAErF,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAG5D,sEAAsE;AACtE,MAAM,GAAG,GAAG,CAAC,GAAW,EAAE,IAAc,EAAE,EAAE,GAAG,MAAM,EAAmB,EAAE,CACxE,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;IAC9B,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QAC7F,IAAI,CAAC,KAAK;YAAE,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC3C,kEAAkE;QAClE,wEAAwE;QACxE,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,CAAC,OAAO,CAAC;QACpD,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEL;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,UAAkB,EAAU,EAAE,CACxD,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,GAAG,QAAQ,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC;AAEjE,mFAAmF;AACnF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAc,EAAU,EAAE,CACpD,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AAEtD;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO;QACL,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YACxB,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE,iBAAiB,CAAC,EAAE,KAAK,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YAC7F,MAAM,IAAI,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;YAC3B,OAAO,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9D,CAAC;QAED,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;YAC7B;;;;;;eAMG;YACH,MAAM,IAAI,GAAG,MAAM,GAAG,CACpB,UAAU,EACV,CAAC,cAAc,EAAE,uBAAuB,EAAE,2BAA2B,EAAE,YAAY,EAAE,cAAc,CAAC,EACpG,MAAM,CACP,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAClB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;YAC/B,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;gBACpC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;gBACzB,qEAAqE;gBACrE,kEAAkE;gBAClE,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;oBAAE,SAAS;gBACpD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC;YAC1C,CAAC;YACD,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;QACnB,CAAC;QAED,MAAM,EAAE,KAAK,EAAE,QAAkB,EAAE,EAAE;YACnC,MAAM,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YACzD;;;;;;;;;;;;eAYG;YACH,MAAM,GAAG,CAAC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,MAAM,KAAK,SAAS;gBAC1D,CAAC,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;gBACnD,CAAC,CAAC;oBACA,UAAU,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,YAAY;oBACrE,IAAI,EAAE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI;iBACpD,CAAC,CAAC;YACL,KAAK,MAAM,OAAO,IAAI,QAAQ,CAAC,OAAO,IAAI,EAAE,EAAE,CAAC;gBAC7C,oEAAoE;gBACpE,gEAAgE;gBAChE,4DAA4D;gBAC5D,MAAM,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YACjF,CAAC;QACH,CAAC;QAED,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YACpB,wEAAwE;YACxE,mEAAmE;YACnE,iEAAiE;YACjE,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YACvF,OAAO,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;QAC5B,CAAC;QAED,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;YACzC,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;YACpD;;;;;;;;eAQG;YACH,IAAI,MAAM,KAAK,SAAS;gBAAE,MAAM,GAAG,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACnG,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,IAAI,GAAG,KAAK,EAAE,IAAY,EAAE,EAAU,EAAE,OAAe,EAAiB,EAAE;IAC9E,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO;IAC9D,MAAM,IAAI,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC;QAChB,MAAM,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAC;QAC3F,OAAO;IACT,CAAC;IACD,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAClD,MAAM,IAAI,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACzD,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC;QAAE,OAAO;IACxD,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAc,CAAC,CAAC;IAC3E,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,IAAI,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,IAAI,EAAE,CAAC;YAAE,SAAS;QAC9E,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QACtC,yEAAyE;QACzE,uEAAuE;QACvE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACvD,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,WAAW,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC,CAAC;IACjG,CAAC;AACH,CAAC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ahpd/server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A server library for the Agent Host Protocol. No agent inside, you pass one in",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"ahp",
|
|
8
|
+
"agent-host-protocol",
|
|
9
|
+
"agent-host",
|
|
10
|
+
"agent",
|
|
11
|
+
"ai",
|
|
12
|
+
"protocol",
|
|
13
|
+
"json-rpc",
|
|
14
|
+
"websocket",
|
|
15
|
+
"server",
|
|
16
|
+
"host",
|
|
17
|
+
"daemon"
|
|
18
|
+
],
|
|
19
|
+
"author": "Luiz Fernando Softov <lfs@softov.dev>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/softov/ahpd#readme",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/softov/ahpd.git",
|
|
28
|
+
"directory": "packages/server"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/softov/ahpd/issues"
|
|
32
|
+
},
|
|
33
|
+
"main": "./dist/index.js",
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"development": "./src/index.ts",
|
|
39
|
+
"default": "./dist/index.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"src",
|
|
45
|
+
"README.md"
|
|
46
|
+
],
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22"
|
|
49
|
+
},
|
|
50
|
+
"optionalDependencies": {
|
|
51
|
+
"ws": "^8.18.0",
|
|
52
|
+
"node-pty": "^1.1.0-beta40"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"@microsoft/agent-host-protocol": "^0.9.0"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@microsoft/agent-host-protocol": "^0.9.0"
|
|
59
|
+
},
|
|
60
|
+
"scripts": {}
|
|
61
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/** Automations held in memory, run when somebody asks. */
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import type { Automation, AutomationRun, AutomationStore, StartSession } from './types/automations.js';
|
|
5
|
+
import type { Bag } from './types/common.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The automations of a host that does not schedule.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately half a store, and the half worth having first: definitions can
|
|
11
|
+
* be written, patched, listed and *run*, and nothing here holds a clock. An
|
|
12
|
+
* automation with a schedule trigger is accepted, kept and reported with no
|
|
13
|
+
* `nextRunAt` - which is the honest form of "this host will not fire that",
|
|
14
|
+
* and is what stops a client drawing a next-run time that will never arrive.
|
|
15
|
+
*
|
|
16
|
+
* Everything is in memory, so it goes when the process does. A store that
|
|
17
|
+
* outlives a restart is a different implementation of the same interface, and
|
|
18
|
+
* the reason the interface exists.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const now = (): string => new Date().toISOString();
|
|
22
|
+
|
|
23
|
+
/** How many runs a summary list carries before it needs a cursor. */
|
|
24
|
+
const PAGE = 20;
|
|
25
|
+
|
|
26
|
+
export function memoryAutomations(): AutomationStore {
|
|
27
|
+
const held = new Map<string, Automation>();
|
|
28
|
+
/** Runs by automation, newest first. */
|
|
29
|
+
const history = new Map<string, AutomationRun[]>();
|
|
30
|
+
/** Every run by its own URI, for the channel a client watches it on. */
|
|
31
|
+
const byRun = new Map<string, AutomationRun>();
|
|
32
|
+
const listeners: ((event: { automation?: string; run?: string; removed?: string }) => void)[] = [];
|
|
33
|
+
const said = (event: { automation?: string; run?: string; removed?: string }): void => {
|
|
34
|
+
for (const listener of listeners) listener(event);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** The summary form of a run, which is what an automation carries. */
|
|
38
|
+
const summary = (run: AutomationRun): Bag => ({
|
|
39
|
+
resource: run.resource,
|
|
40
|
+
automation: run.automation,
|
|
41
|
+
origin: run.origin,
|
|
42
|
+
lifecycle: run.lifecycle,
|
|
43
|
+
sessionCount: run.sessions.length,
|
|
44
|
+
...(run.primarySession !== undefined ? { primarySession: run.primarySession } : {}),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/** Rebuild the entry a client reads, so `runs` and `operations` are never stale. */
|
|
48
|
+
const entry = (automation: Automation): Automation => {
|
|
49
|
+
const enabled = automation.definition.enabled !== false;
|
|
50
|
+
return {
|
|
51
|
+
...automation,
|
|
52
|
+
runs: (history.get(automation.resource) ?? []).slice(0, PAGE).map(summary),
|
|
53
|
+
...((history.get(automation.resource) ?? []).length > PAGE
|
|
54
|
+
? { runsNextCursor: String(PAGE) }
|
|
55
|
+
: {}),
|
|
56
|
+
// `run` only where it would do something. A disabled automation is one
|
|
57
|
+
// somebody switched off, and offering the button anyway is a control
|
|
58
|
+
// that argues with the switch beside it.
|
|
59
|
+
operations: enabled ? ['update', 'remove', 'run'] : ['update', 'remove'],
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
list: () => [...held.values()].map(entry),
|
|
65
|
+
get: (resource) => {
|
|
66
|
+
const found = held.get(resource);
|
|
67
|
+
return found && entry(found);
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
/*
|
|
71
|
+
* What this store understands, which is nothing.
|
|
72
|
+
*
|
|
73
|
+
* This command answers with *event* triggers. Schedule triggers are
|
|
74
|
+
* protocol-defined and never listed here, and manual is not a trigger at
|
|
75
|
+
* all - the protocol says an empty trigger list is what manual-only means.
|
|
76
|
+
* So a store with no event triggers answers with none, and a client reads
|
|
77
|
+
* "this host will not fire that" from the absent `nextRunAt` instead.
|
|
78
|
+
*/
|
|
79
|
+
triggers: () => [],
|
|
80
|
+
|
|
81
|
+
create: (resource, definition) => {
|
|
82
|
+
const at = now();
|
|
83
|
+
const made: Automation = {
|
|
84
|
+
resource,
|
|
85
|
+
definition,
|
|
86
|
+
runs: [],
|
|
87
|
+
operations: [],
|
|
88
|
+
createdAt: at,
|
|
89
|
+
modifiedAt: at,
|
|
90
|
+
};
|
|
91
|
+
held.set(resource, made);
|
|
92
|
+
said({ automation: resource });
|
|
93
|
+
return entry(made);
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
update: (resource, changes) => {
|
|
97
|
+
const found = held.get(resource);
|
|
98
|
+
if (!found) return undefined;
|
|
99
|
+
// A patch: absent keys are left alone, which is the whole difference
|
|
100
|
+
// between this and a write. A client sending the whole definition back
|
|
101
|
+
// would otherwise revert whatever another client changed meanwhile.
|
|
102
|
+
const after: Automation = {
|
|
103
|
+
...found,
|
|
104
|
+
definition: { ...found.definition, ...changes },
|
|
105
|
+
modifiedAt: now(),
|
|
106
|
+
};
|
|
107
|
+
held.set(resource, after);
|
|
108
|
+
said({ automation: resource });
|
|
109
|
+
return entry(after);
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
remove: (resource) => {
|
|
113
|
+
if (!held.delete(resource)) return false;
|
|
114
|
+
for (const run of history.get(resource) ?? []) byRun.delete(run.resource);
|
|
115
|
+
history.delete(resource);
|
|
116
|
+
said({ removed: resource });
|
|
117
|
+
return true;
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
run: async (resource, origin, start) => {
|
|
121
|
+
const found = held.get(resource);
|
|
122
|
+
if (!found) return undefined;
|
|
123
|
+
if (found.definition.enabled === false) return undefined;
|
|
124
|
+
const template = (typeof found.definition.session === 'object' && found.definition.session !== null
|
|
125
|
+
? found.definition.session
|
|
126
|
+
: {}) as Bag;
|
|
127
|
+
const message = (typeof found.definition.message === 'object' && found.definition.message !== null
|
|
128
|
+
? found.definition.message
|
|
129
|
+
: {}) as Bag;
|
|
130
|
+
|
|
131
|
+
const run: AutomationRun = {
|
|
132
|
+
resource: `ahp-automation-run:/${randomUUID()}`,
|
|
133
|
+
automation: resource,
|
|
134
|
+
origin,
|
|
135
|
+
lifecycle: { status: 'pending', createdAt: now() },
|
|
136
|
+
sessions: [],
|
|
137
|
+
};
|
|
138
|
+
byRun.set(run.resource, run);
|
|
139
|
+
const past = history.get(resource) ?? [];
|
|
140
|
+
// Newest first, which is the order a client shows them in.
|
|
141
|
+
history.set(resource, [run, ...past]);
|
|
142
|
+
said({ automation: resource, run: run.resource });
|
|
143
|
+
|
|
144
|
+
const directories = Array.isArray(template.workingDirectories) ? template.workingDirectories : [];
|
|
145
|
+
const where = typeof directories[0] === 'string'
|
|
146
|
+
? (directories[0] as string).replace(/^file:\/\//, '')
|
|
147
|
+
: undefined;
|
|
148
|
+
const options: StartSession = {
|
|
149
|
+
...(typeof template.provider === 'string' ? { provider: template.provider } : {}),
|
|
150
|
+
...(where !== undefined ? { workingDirectory: where } : {}),
|
|
151
|
+
...(typeof template.config === 'object' && template.config !== null
|
|
152
|
+
? { config: template.config as Record<string, string> }
|
|
153
|
+
: {}),
|
|
154
|
+
text: typeof message.text === 'string' ? message.text : String(found.definition.title ?? ''),
|
|
155
|
+
origin: { kind: 'automation', automation: resource, run: run.resource },
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const session = await start(options);
|
|
160
|
+
run.sessions = [session];
|
|
161
|
+
run.primarySession = session;
|
|
162
|
+
run.lifecycle = { ...run.lifecycle, status: 'running', startedAt: now() };
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
// Failed, and why. A run that vanished would be indistinguishable from
|
|
166
|
+
// one that never started.
|
|
167
|
+
run.lifecycle = {
|
|
168
|
+
...run.lifecycle,
|
|
169
|
+
status: 'failed',
|
|
170
|
+
endedAt: now(),
|
|
171
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
said({ automation: resource, run: run.resource });
|
|
175
|
+
return run;
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
runOf: (resource) => byRun.get(resource),
|
|
179
|
+
|
|
180
|
+
unlink: (resource, session) => {
|
|
181
|
+
const run = byRun.get(resource);
|
|
182
|
+
if (run === undefined || !run.sessions.includes(session)) return false;
|
|
183
|
+
run.sessions = run.sessions.filter((one) => one !== session);
|
|
184
|
+
// The protocol says removing the primary clears it. A run pointing at a
|
|
185
|
+
// session that is gone is a run a client opens onto nothing.
|
|
186
|
+
if (run.primarySession === session) delete run.primarySession;
|
|
187
|
+
said({ automation: run.automation, run: run.resource });
|
|
188
|
+
return true;
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
runs: (resource, cursor) => {
|
|
192
|
+
const all = history.get(resource) ?? [];
|
|
193
|
+
const from = cursor === undefined ? 0 : Number(cursor);
|
|
194
|
+
const at = Number.isFinite(from) && from >= 0 ? from : 0;
|
|
195
|
+
const page = all.slice(at, at + PAGE);
|
|
196
|
+
return {
|
|
197
|
+
items: page.map(summary),
|
|
198
|
+
...(at + PAGE < all.length ? { nextCursor: String(at + PAGE) } : {}),
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
onChanged: (observer) => { listeners.push(observer); },
|
|
203
|
+
};
|
|
204
|
+
}
|
package/src/catalog.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How a session is named, and what its status bits are worth.
|
|
3
|
+
*
|
|
4
|
+
* URIs and numbers, and nothing that knows what a session *is*: the listing
|
|
5
|
+
* itself belongs to whichever backend wrote the sessions down, because only
|
|
6
|
+
* that one can say which exist. See `catalogue` in `@ahpd/agent-claude`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `SessionStatus`, as values.
|
|
11
|
+
*
|
|
12
|
+
* The protocol declares it as a `const enum`, which exists only in the type
|
|
13
|
+
* system - importing it at runtime is a value import of something that was
|
|
14
|
+
* erased. So the numbers are restated here, and they are the protocol's:
|
|
15
|
+
* `InputNeeded` is 24 and *carries* `InProgress` (8), which is why anything
|
|
16
|
+
* testing activity has to test it first.
|
|
17
|
+
*/
|
|
18
|
+
export const Status = {
|
|
19
|
+
Idle: 1,
|
|
20
|
+
Error: 2,
|
|
21
|
+
InProgress: 8,
|
|
22
|
+
InputNeeded: 24,
|
|
23
|
+
IsRead: 32,
|
|
24
|
+
IsArchived: 64,
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A URI for a session this host found on disk. `ahp-session:/<uuid>`, and the
|
|
29
|
+
* SDK's id is already a uuid.
|
|
30
|
+
*
|
|
31
|
+
* Only for rows this host names itself. A session a *client* created is named
|
|
32
|
+
* by that client, in whatever scheme it likes - see {@link idOf}.
|
|
33
|
+
*/
|
|
34
|
+
export const uriFor = (sessionId: string): string => `ahp-session:/${sessionId}`;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The id inside a channel URI, whatever scheme the client chose.
|
|
38
|
+
*
|
|
39
|
+
* A channel URI is the **client's** to name and this host's to echo. VS Code
|
|
40
|
+
* names a session after its provider - `claude:/<uuid>`, with the provider as
|
|
41
|
+
* the *scheme* - and its terminals `agenthost-terminal:/<uuid>`. This used to
|
|
42
|
+
* strip a literal `ahp-session:/` and refuse anything else, so every session
|
|
43
|
+
* and every terminal VS Code opened was answered `is not a session URI`.
|
|
44
|
+
*
|
|
45
|
+
* So: everything after the scheme, without its leading slashes. The result is
|
|
46
|
+
* an opaque key, not something to parse further.
|
|
47
|
+
*/
|
|
48
|
+
export const idOf = (uri: string): string => {
|
|
49
|
+
const colon = uri.indexOf(':');
|
|
50
|
+
return (colon < 0 ? uri : uri.slice(colon + 1)).replace(/^\/+/, '');
|
|
51
|
+
};
|
|
52
|
+
/** The same, and the name it goes by where a session rather than a chat is meant. */
|
|
53
|
+
export const idFor = idOf;
|