@residue/cli 0.0.1 → 0.0.2
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/CHANGELOG.md +22 -0
- package/README.md +44 -0
- package/dist/index.js +37 -9
- package/package.json +6 -1
- package/src/commands/login.ts +13 -2
- package/src/commands/sync.ts +2 -2
- package/src/index.ts +3 -2
- package/src/lib/config.ts +70 -10
- package/dist/residue +0 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @residue/cli
|
|
2
|
+
|
|
3
|
+
## 0.0.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Add per-project login support with `--local` flag. Running `residue login --local` saves config to `.residue/config` in the project root instead of the global `~/.residue/config`. The sync command now resolves config locally first before falling back to global.
|
|
8
|
+
|
|
9
|
+
## 0.0.1
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- Initial release
|
|
14
|
+
- `residue login` to save worker URL and auth token
|
|
15
|
+
- `residue init` to install git hooks (post-commit, pre-push)
|
|
16
|
+
- `residue setup` for agent adapters (claude-code, pi)
|
|
17
|
+
- `residue capture` for post-commit session tagging
|
|
18
|
+
- `residue sync` for pre-push session upload via presigned R2 URLs
|
|
19
|
+
- `residue push` as manual sync alias
|
|
20
|
+
- `residue session start` and `residue session end` for agent adapters
|
|
21
|
+
- `residue hook claude-code` for Claude Code native hook protocol
|
|
22
|
+
- Claude Code and Pi agent adapters
|
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# @residue/cli
|
|
2
|
+
|
|
3
|
+
CLI that captures AI agent conversations and links them to git commits.
|
|
4
|
+
|
|
5
|
+
Part of [residue](https://residue.dev) -- see the full docs at [residue.dev](https://residue.dev).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add -g @residue/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Save your worker URL and auth token
|
|
17
|
+
residue login --url https://your-worker.workers.dev --token YOUR_TOKEN
|
|
18
|
+
|
|
19
|
+
# Install git hooks in your repo
|
|
20
|
+
residue init
|
|
21
|
+
|
|
22
|
+
# Set up an agent adapter
|
|
23
|
+
residue setup claude-code # for Claude Code
|
|
24
|
+
residue setup pi # for pi coding agent
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
After setup, conversations are captured automatically. Commit and push as usual.
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
| Command | Description |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `residue login` | Save worker URL + auth token |
|
|
34
|
+
| `residue init` | Install git hooks (post-commit, pre-push) |
|
|
35
|
+
| `residue setup <agent>` | Configure an agent adapter |
|
|
36
|
+
| `residue push` | Manually upload pending sessions |
|
|
37
|
+
| `residue capture` | Tag pending sessions with current commit (hook) |
|
|
38
|
+
| `residue sync` | Upload sessions to worker (hook) |
|
|
39
|
+
| `residue session start` | Register a new session (adapter) |
|
|
40
|
+
| `residue session end` | Mark a session as ended (adapter) |
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -3895,10 +3895,9 @@ function getConfigDir() {
|
|
|
3895
3895
|
function getConfigPath() {
|
|
3896
3896
|
return join4(getConfigDir(), "config");
|
|
3897
3897
|
}
|
|
3898
|
-
function
|
|
3898
|
+
function readConfigFromPath(configPath) {
|
|
3899
3899
|
return ResultAsync.fromPromise((async () => {
|
|
3900
|
-
const
|
|
3901
|
-
const file = Bun.file(path);
|
|
3900
|
+
const file = Bun.file(configPath);
|
|
3902
3901
|
const isExists = await file.exists();
|
|
3903
3902
|
if (!isExists)
|
|
3904
3903
|
return null;
|
|
@@ -3906,13 +3905,36 @@ function readConfig() {
|
|
|
3906
3905
|
return JSON.parse(text);
|
|
3907
3906
|
})(), toCliError({ message: "Failed to read config", code: "CONFIG_ERROR" }));
|
|
3908
3907
|
}
|
|
3909
|
-
function
|
|
3908
|
+
function writeConfigToPath(opts) {
|
|
3910
3909
|
return ResultAsync.fromPromise((async () => {
|
|
3911
|
-
const dir =
|
|
3910
|
+
const dir = join4(opts.configPath, "..");
|
|
3912
3911
|
await mkdir4(dir, { recursive: true });
|
|
3913
|
-
await Bun.write(
|
|
3912
|
+
await Bun.write(opts.configPath, JSON.stringify(opts.config, null, 2));
|
|
3914
3913
|
})(), toCliError({ message: "Failed to write config", code: "CONFIG_ERROR" }));
|
|
3915
3914
|
}
|
|
3915
|
+
function readConfig() {
|
|
3916
|
+
return readConfigFromPath(getConfigPath());
|
|
3917
|
+
}
|
|
3918
|
+
function writeConfig(config) {
|
|
3919
|
+
return writeConfigToPath({ configPath: getConfigPath(), config });
|
|
3920
|
+
}
|
|
3921
|
+
function readLocalConfig(projectRoot) {
|
|
3922
|
+
return readConfigFromPath(join4(projectRoot, ".residue", "config"));
|
|
3923
|
+
}
|
|
3924
|
+
function writeLocalConfig(opts) {
|
|
3925
|
+
return writeConfigToPath({
|
|
3926
|
+
configPath: join4(opts.projectRoot, ".residue", "config"),
|
|
3927
|
+
config: opts.config
|
|
3928
|
+
});
|
|
3929
|
+
}
|
|
3930
|
+
function resolveConfig() {
|
|
3931
|
+
return getProjectRoot().andThen((projectRoot) => readLocalConfig(projectRoot)).orElse(() => okAsync(null)).andThen((localConfig) => {
|
|
3932
|
+
if (localConfig !== null) {
|
|
3933
|
+
return okAsync(localConfig);
|
|
3934
|
+
}
|
|
3935
|
+
return readConfig();
|
|
3936
|
+
});
|
|
3937
|
+
}
|
|
3916
3938
|
|
|
3917
3939
|
// src/commands/login.ts
|
|
3918
3940
|
var log4 = createLogger("login");
|
|
@@ -3924,7 +3946,13 @@ function login(opts) {
|
|
|
3924
3946
|
}));
|
|
3925
3947
|
}
|
|
3926
3948
|
const cleanUrl = opts.url.replace(/\/+$/, "");
|
|
3927
|
-
|
|
3949
|
+
const config = { worker_url: cleanUrl, token: opts.token };
|
|
3950
|
+
if (opts.isLocal) {
|
|
3951
|
+
return getProjectRoot().andThen((projectRoot) => writeLocalConfig({ projectRoot, config }).map(() => {
|
|
3952
|
+
log4.info(`Logged in to ${cleanUrl} (project-local config)`);
|
|
3953
|
+
}));
|
|
3954
|
+
}
|
|
3955
|
+
return writeConfig(config).map(() => {
|
|
3928
3956
|
log4.info(`Logged in to ${cleanUrl}`);
|
|
3929
3957
|
});
|
|
3930
3958
|
}
|
|
@@ -4117,7 +4145,7 @@ function resolveRemote(remoteUrl) {
|
|
|
4117
4145
|
}
|
|
4118
4146
|
function sync(opts) {
|
|
4119
4147
|
return safeTry(async function* () {
|
|
4120
|
-
const config = yield*
|
|
4148
|
+
const config = yield* resolveConfig();
|
|
4121
4149
|
if (!config) {
|
|
4122
4150
|
return err(new CliError({
|
|
4123
4151
|
message: "Not configured. Run 'residue login' first.",
|
|
@@ -4425,7 +4453,7 @@ function setup(opts) {
|
|
|
4425
4453
|
// src/index.ts
|
|
4426
4454
|
var program2 = new Command;
|
|
4427
4455
|
program2.name("residue").description("Capture AI agent conversations linked to git commits").version("0.0.1");
|
|
4428
|
-
program2.command("login").description("Save worker URL and auth token").requiredOption("--url <worker_url>", "Worker URL").requiredOption("--token <auth_token>", "Auth token").action(wrapCommand((opts) => login({ url: opts.url, token: opts.token })));
|
|
4456
|
+
program2.command("login").description("Save worker URL and auth token").requiredOption("--url <worker_url>", "Worker URL").requiredOption("--token <auth_token>", "Auth token").option("--local", "Save config to this project instead of globally").action(wrapCommand((opts) => login({ url: opts.url, token: opts.token, isLocal: opts.local })));
|
|
4429
4457
|
program2.command("init").description("Install git hooks in current repo").action(wrapCommand(() => init()));
|
|
4430
4458
|
program2.command("setup").description("Configure an agent adapter for this project").argument("<agent>", "Agent to set up (claude-code, pi)").action(wrapCommand((agent) => setup({ agent })));
|
|
4431
4459
|
var hook = program2.command("hook").description("Agent hook handlers (called by agent plugins)");
|
package/package.json
CHANGED
package/src/commands/login.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { errAsync, type ResultAsync } from "neverthrow";
|
|
2
|
-
import { writeConfig } from "@/lib/config";
|
|
2
|
+
import { writeConfig, writeLocalConfig } from "@/lib/config";
|
|
3
|
+
import { getProjectRoot } from "@/lib/pending";
|
|
3
4
|
import { CliError } from "@/utils/errors";
|
|
4
5
|
import { createLogger } from "@/utils/logger";
|
|
5
6
|
|
|
@@ -8,6 +9,7 @@ const log = createLogger("login");
|
|
|
8
9
|
export function login(opts: {
|
|
9
10
|
url: string;
|
|
10
11
|
token: string;
|
|
12
|
+
isLocal?: boolean;
|
|
11
13
|
}): ResultAsync<void, CliError> {
|
|
12
14
|
if (!opts.url.startsWith("http://") && !opts.url.startsWith("https://")) {
|
|
13
15
|
return errAsync(
|
|
@@ -19,8 +21,17 @@ export function login(opts: {
|
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
const cleanUrl = opts.url.replace(/\/+$/, "");
|
|
24
|
+
const config = { worker_url: cleanUrl, token: opts.token };
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
if (opts.isLocal) {
|
|
27
|
+
return getProjectRoot().andThen((projectRoot) =>
|
|
28
|
+
writeLocalConfig({ projectRoot, config }).map(() => {
|
|
29
|
+
log.info(`Logged in to ${cleanUrl} (project-local config)`);
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return writeConfig(config).map(() => {
|
|
24
35
|
log.info(`Logged in to ${cleanUrl}`);
|
|
25
36
|
});
|
|
26
37
|
}
|
package/src/commands/sync.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { err, ok, okAsync, ResultAsync, safeTry } from "neverthrow";
|
|
2
|
-
import {
|
|
2
|
+
import { resolveConfig } from "@/lib/config";
|
|
3
3
|
import { getCommitMeta, getRemoteUrl, parseRemote } from "@/lib/git";
|
|
4
4
|
import type { CommitRef, PendingSession } from "@/lib/pending";
|
|
5
5
|
import {
|
|
@@ -320,7 +320,7 @@ export function sync(opts?: {
|
|
|
320
320
|
remoteUrl?: string;
|
|
321
321
|
}): ResultAsync<void, CliError> {
|
|
322
322
|
return safeTry(async function* () {
|
|
323
|
-
const config = yield*
|
|
323
|
+
const config = yield* resolveConfig();
|
|
324
324
|
if (!config) {
|
|
325
325
|
return err(
|
|
326
326
|
new CliError({
|
package/src/index.ts
CHANGED
|
@@ -24,9 +24,10 @@ program
|
|
|
24
24
|
.description("Save worker URL and auth token")
|
|
25
25
|
.requiredOption("--url <worker_url>", "Worker URL")
|
|
26
26
|
.requiredOption("--token <auth_token>", "Auth token")
|
|
27
|
+
.option("--local", "Save config to this project instead of globally")
|
|
27
28
|
.action(
|
|
28
|
-
wrapCommand((opts: { url: string; token: string }) =>
|
|
29
|
-
login({ url: opts.url, token: opts.token }),
|
|
29
|
+
wrapCommand((opts: { url: string; token: string; local?: boolean }) =>
|
|
30
|
+
login({ url: opts.url, token: opts.token, isLocal: opts.local }),
|
|
30
31
|
),
|
|
31
32
|
);
|
|
32
33
|
|
package/src/lib/config.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Config management for the residue CLI.
|
|
3
|
-
*
|
|
3
|
+
* Global config: ~/.residue/config
|
|
4
|
+
* Local (per-project) config: .residue/config (in project root)
|
|
5
|
+
*
|
|
6
|
+
* resolveConfig() checks local first, then falls back to global.
|
|
4
7
|
*/
|
|
5
8
|
|
|
6
9
|
import { mkdir } from "fs/promises";
|
|
7
|
-
import { ResultAsync } from "neverthrow";
|
|
10
|
+
import { okAsync, ResultAsync } from "neverthrow";
|
|
8
11
|
import { join } from "path";
|
|
12
|
+
import { getProjectRoot } from "@/lib/pending";
|
|
9
13
|
import type { CliError } from "@/utils/errors";
|
|
10
14
|
import { toCliError } from "@/utils/errors";
|
|
11
15
|
|
|
@@ -26,11 +30,12 @@ export function getConfigPath(): string {
|
|
|
26
30
|
return join(getConfigDir(), "config");
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
function readConfigFromPath(
|
|
34
|
+
configPath: string,
|
|
35
|
+
): ResultAsync<ResidueConfig | null, CliError> {
|
|
30
36
|
return ResultAsync.fromPromise(
|
|
31
37
|
(async () => {
|
|
32
|
-
const
|
|
33
|
-
const file = Bun.file(path);
|
|
38
|
+
const file = Bun.file(configPath);
|
|
34
39
|
const isExists = await file.exists();
|
|
35
40
|
if (!isExists) return null;
|
|
36
41
|
const text = await file.text();
|
|
@@ -40,19 +45,74 @@ export function readConfig(): ResultAsync<ResidueConfig | null, CliError> {
|
|
|
40
45
|
);
|
|
41
46
|
}
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
48
|
+
function writeConfigToPath(opts: {
|
|
49
|
+
configPath: string;
|
|
50
|
+
config: ResidueConfig;
|
|
51
|
+
}): ResultAsync<void, CliError> {
|
|
46
52
|
return ResultAsync.fromPromise(
|
|
47
53
|
(async () => {
|
|
48
|
-
const dir =
|
|
54
|
+
const dir = join(opts.configPath, "..");
|
|
49
55
|
await mkdir(dir, { recursive: true });
|
|
50
|
-
await Bun.write(
|
|
56
|
+
await Bun.write(opts.configPath, JSON.stringify(opts.config, null, 2));
|
|
51
57
|
})(),
|
|
52
58
|
toCliError({ message: "Failed to write config", code: "CONFIG_ERROR" }),
|
|
53
59
|
);
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Read the global config from ~/.residue/config.
|
|
64
|
+
*/
|
|
65
|
+
export function readConfig(): ResultAsync<ResidueConfig | null, CliError> {
|
|
66
|
+
return readConfigFromPath(getConfigPath());
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Write the global config to ~/.residue/config.
|
|
71
|
+
*/
|
|
72
|
+
export function writeConfig(
|
|
73
|
+
config: ResidueConfig,
|
|
74
|
+
): ResultAsync<void, CliError> {
|
|
75
|
+
return writeConfigToPath({ configPath: getConfigPath(), config });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Read the local (per-project) config from .residue/config.
|
|
80
|
+
*/
|
|
81
|
+
export function readLocalConfig(
|
|
82
|
+
projectRoot: string,
|
|
83
|
+
): ResultAsync<ResidueConfig | null, CliError> {
|
|
84
|
+
return readConfigFromPath(join(projectRoot, ".residue", "config"));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Write the local (per-project) config to .residue/config.
|
|
89
|
+
*/
|
|
90
|
+
export function writeLocalConfig(opts: {
|
|
91
|
+
projectRoot: string;
|
|
92
|
+
config: ResidueConfig;
|
|
93
|
+
}): ResultAsync<void, CliError> {
|
|
94
|
+
return writeConfigToPath({
|
|
95
|
+
configPath: join(opts.projectRoot, ".residue", "config"),
|
|
96
|
+
config: opts.config,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve config by checking local (per-project) first, then global.
|
|
102
|
+
* Returns the first one found, or null if neither exists.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveConfig(): ResultAsync<ResidueConfig | null, CliError> {
|
|
105
|
+
return getProjectRoot()
|
|
106
|
+
.andThen((projectRoot) => readLocalConfig(projectRoot))
|
|
107
|
+
.orElse(() => okAsync(null as ResidueConfig | null))
|
|
108
|
+
.andThen((localConfig) => {
|
|
109
|
+
if (localConfig !== null) {
|
|
110
|
+
return okAsync(localConfig as ResidueConfig | null);
|
|
111
|
+
}
|
|
112
|
+
return readConfig();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
56
116
|
export function configExists(): ResultAsync<boolean, CliError> {
|
|
57
117
|
return ResultAsync.fromPromise(
|
|
58
118
|
Bun.file(getConfigPath()).exists(),
|
package/dist/residue
DELETED
|
Binary file
|