@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 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 readConfig() {
3898
+ function readConfigFromPath(configPath) {
3899
3899
  return ResultAsync.fromPromise((async () => {
3900
- const path = getConfigPath();
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 writeConfig(config) {
3908
+ function writeConfigToPath(opts) {
3910
3909
  return ResultAsync.fromPromise((async () => {
3911
- const dir = getConfigDir();
3910
+ const dir = join4(opts.configPath, "..");
3912
3911
  await mkdir4(dir, { recursive: true });
3913
- await Bun.write(getConfigPath(), JSON.stringify(config, null, 2));
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
- return writeConfig({ worker_url: cleanUrl, token: opts.token }).map(() => {
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* readConfig();
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
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@residue/cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/butttons/residue",
7
+ "directory": "packages/cli"
8
+ },
4
9
  "type": "module",
5
10
  "bin": {
6
11
  "residue": "./dist/index.js"
@@ -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
- return writeConfig({ worker_url: cleanUrl, token: opts.token }).map(() => {
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
  }
@@ -1,5 +1,5 @@
1
1
  import { err, ok, okAsync, ResultAsync, safeTry } from "neverthrow";
2
- import { readConfig } from "@/lib/config";
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* readConfig();
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
- * Manages ~/.residue/config (JSON file).
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
- export function readConfig(): ResultAsync<ResidueConfig | null, CliError> {
33
+ function readConfigFromPath(
34
+ configPath: string,
35
+ ): ResultAsync<ResidueConfig | null, CliError> {
30
36
  return ResultAsync.fromPromise(
31
37
  (async () => {
32
- const path = getConfigPath();
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
- export function writeConfig(
44
- config: ResidueConfig,
45
- ): ResultAsync<void, CliError> {
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 = getConfigDir();
54
+ const dir = join(opts.configPath, "..");
49
55
  await mkdir(dir, { recursive: true });
50
- await Bun.write(getConfigPath(), JSON.stringify(config, null, 2));
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