@harnessforce/cli 0.1.1 → 0.3.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.
Files changed (3) hide show
  1. package/README.md +63 -39
  2. package/dist/bin.js +1648 -1190
  3. package/package.json +9 -9
package/README.md CHANGED
@@ -23,18 +23,19 @@ harnessforce init
23
23
 
24
24
  ## Commands
25
25
 
26
+ `harnessforce --help` lists the commands. `harnessforce <command> --help` explains when to use a command, what it sends and does not send, its exit codes, and examples.
27
+
26
28
  | Command | What it does |
27
29
  | --- | --- |
28
- | `harnessforce init [--url <base URL>] [--port <port>] [--send-content]` | Logs in with the browser, stores a user ingest key and an API token in the keychain, and configures Claude Code and Codex |
29
- | `harnessforce otel-headers` | Prints the `Authorization` header for Claude Code's `otelHeadersHelper` |
30
+ | `harnessforce init [--port <port>] [--send-content] [--no-plugin-auto-update]` | Logs in with the browser, stores a user ingest key and an API token in the keychain, and configures Claude Code and Codex |
31
+ | `harnessforce otel-headers` | Prints the `Authorization` header for Claude Code's `otelHeadersHelper`. Exits 3 when there is no key, 4 when the destination differs from the pinned origin, 5 while the CLI version is unsupported, and 1 on other failures. On Linux, after printing a key read from the kernel keyring, it moves that Workspace's items to an unlocked Secret Service (not when the hook starts it), so a reboot does not lose them |
30
32
  | `harnessforce run --issue <identifier> -- <agent> [args]` | Launches an agent with its session linked to an Issue |
31
33
  | `harnessforce import` | Sends the metadata of past Claude Code sessions in connected repositories |
32
34
  | `harnessforce tune [--all] [--no-send] [--show-report] [--json]` | Analyzes past sessions on this machine by public rules and sends the counts (analysis reports) |
33
35
  | `harnessforce tune record` / `harnessforce tune --purge` | Records a proposal from stdin / deletes everything under `~/.harnessforce/tune/` |
34
36
  | `harnessforce top [--once] [--json] [--ascii] [--theme <auto\|light\|dark\|ansi>]` | Shows the Claude Code and Codex sessions on this machine in the terminal: state, tokens, context and tool failures. Reads only local files and never connects to Harnessforce |
35
37
  | `harnessforce hook session-start` | The entry of Codex's `SessionStart` hook: reads `session_id`, `cwd` and `source` from stdin, registers the session, and prints the session ID for the agent's context. Always exits 0 |
36
- | `harnessforce usage-limits on [--yes]` / `off` / `status` | Shares your usage limits with the workspace after you agree to the consent text (`on`), or withdraws the consent, deletes the stored values and restores your previous statusLine (`off`), or shows the consent, the workspace opt-in and whether the statusLine is wrapped (`status`). Not supported on Windows |
37
- | `harnessforce usage-limits statusline` | A statusLine command that wraps your existing one: it passes stdin to the original command, returns its output and exit code unchanged, then sends only the usage percentage and reset time of the 5-hour and 7-day windows. It sends nothing until you have consented |
38
+ | `harnessforce hook post-tool-use` / `stop` | The entries of Codex's `PostToolUse` (`apply_patch`) and `Stop` hooks: record the repository and branch of the working trees the session works in, and send them at the end of a turn in which they changed. Always exit 0 |
38
39
  | `harnessforce --version` | Prints the version |
39
40
 
40
41
  ## Watching your sessions (`harnessforce top`)
@@ -62,6 +63,20 @@ no context percentage.
62
63
  - `~/.harnessforce/config.json` may set `{"top": {"ambiguous_width": 2, "braille": true}}`. An unreadable file or a
63
64
  value out of range falls back to the defaults and says so; the file is never written.
64
65
 
66
+ ## Codex work location hooks (`harnessforce hook post-tool-use` and `stop`)
67
+
68
+ Codex has no event for a change of the working directory, so these hooks record the repository and branch
69
+ of the working tree of each file named in an `apply_patch` patch (`*** Add File:`, `*** Update File:`,
70
+ `*** Delete File:` and `*** Move to:` lines; relative paths are resolved from `cwd`) and of `cwd` at the end
71
+ of each turn. At `Stop`, when the pairs differ from the pairs sent last, they are sent to
72
+ `/v1/sessions/locations` with the same destination and key as the `SessionStart` hook. Only the agent, the
73
+ session ID and up to 10 pairs of repository and branch are sent; paths, commits, file names and the content
74
+ of patches are not. Codex gives hooks no scratchpad, so the record is kept in
75
+ `<temporary directory>/harnessforce-locations/codex-<session_id>.json`; on macOS and Linux the directory
76
+ must be owned by you with mode `0700`, or nothing is recorded. `stop` always prints `{}`, and both always
77
+ exit 0. A machine set up with an earlier `harnessforce init` gets these hooks when you run it again and
78
+ trust them in `/hooks`.
79
+
65
80
  ## Codex `SessionStart` hook (`harnessforce hook session-start`)
66
81
 
67
82
  Codex runs this command through the `[[hooks.SessionStart]]` entry that `harnessforce init` writes into Codex's
@@ -73,36 +88,28 @@ Codex runs this command through the `[[hooks.SessionStart]]` entry that `harness
73
88
  Claude Code user settings that `harnessforce init` wrote. When `HARNESSFORCE_ISSUE` is a valid identifier
74
89
  (`harnessforce run --issue` sets it), the registration claims `source=cli` with that Issue.
75
90
  - prints `{"hookSpecificOutput": {...}}` carrying `harnessforce session_id: <id>` so the agent knows its session
76
- ID. When the key was revoked (401) it prints that line and the message as plain text instead, and writes the
77
- message to stderr.
91
+ ID. When it cannot send, it adds one warning as `systemMessage`, which Codex shows in its UI, and writes it to
92
+ stderr: a revoked key (401), sending not set up, an invalid endpoint, no key in the keychain, a destination
93
+ that differs from the pinned origin, a keychain that could not be read within 1 second, or a failed
94
+ registration. Only `startup`, `clear` and a missing `source` show a warning.
78
95
  - never fails the session: any error ends with exit code 0, and nothing is sent outside a git repository.
79
96
 
80
- ## Usage limits in the statusLine (`harnessforce usage-limits statusline`)
81
-
82
- Claude Code passes the plan limits (`rate_limits`) only to the statusLine command, not to hooks. `harnessforce usage-limits statusline` is meant to sit in front of the statusLine you already use (for example claude-hud):
83
-
84
- - It reads all of stdin and, if `~/.harnessforce/usage-limits.json` has an `original` command, runs it with `sh -c`, hands it the same stdin and the same environment, and returns its stdout, stderr and exit code as they are. No time limit is added.
85
- - After that, only if `usage-limits.json` records your consent for the current text version, it takes `rate_limits.five_hour` and `rate_limits.seven_day` (`used_percentage` and `resets_at`) and sends them to `POST <ingest endpoint>/v1/usage-limits` with the user ingest key from the keychain. Nothing else in the JSON (`cwd`, `transcript_path`, `cost`, and so on) is read or sent.
86
- - The send runs in a detached child process with stdin, stdout and stderr closed, so the statusLine is never delayed. A failure does not change the output or the exit code.
87
- - It sends at most once every 5 minutes, unless the reset time of a window changed. The time and the reset times of the last send are kept in `~/.harnessforce/usage-limits-state.json` (owner-only).
88
-
89
- ### Turning it on and off
90
-
91
- `harnessforce usage-limits on` shows the consent text, asks `y` (skipped with `--yes`), records the consent (`PUT /api/v1/cli/usage-limits/consent`), and then wraps the `statusLine` in your Claude Code user settings: only `command` is replaced with `harnessforce usage-limits statusline`, so `type`, `padding`, `refreshInterval` and the other keys stay. The statusLine you had (for example claude-hud) is saved as-is in `~/.harnessforce/usage-limits.json`. If the statusLine is not `type: "command"`, or the settings cannot be read, nothing is recorded or changed. If writing the mark or the settings fails, the consent is withdrawn again. `harnessforce usage-limits off` withdraws the consent first and then puts the saved statusLine back (or removes it if there was none); a statusLine that another tool has changed since is left alone.
92
-
93
97
  ## What is never sent
94
98
 
95
99
  Harnessforce never turns on prompt or body logging by itself. `harnessforce run` never turns it on, and `harnessforce init`
96
- adds `OTEL_LOG_USER_PROMPTS=1` to the Claude Code user settings only with `--send-content`, and only
97
- when the credentials response says the Workspace has opted in to content. Without that, prompts,
98
- responses, tool inputs and outputs, and file contents are not sent. For a Workspace that has not opted
99
- in, `harnessforce init --send-content` leaves the setting alone, prints the reason, and still exits 0. `harnessforce run`
100
+ sets `OTEL_LOG_USER_PROMPTS`, `OTEL_LOG_ASSISTANT_RESPONSES`, `OTEL_LOG_TOOL_DETAILS` and
101
+ `OTEL_LOG_TOOL_CONTENT` in the Claude Code user settings to `1` only with `--send-content`, and only for
102
+ the categories the Workspace stores (prompts, and model responses with tool input and output). Without that,
103
+ `harnessforce init` writes all four as `0` (and Codex's `log_user_prompt` and `log_agent_responses` as
104
+ `false`), so a value left from an earlier run cannot keep sending. `OTEL_LOG_RAW_API_BODIES` is never written.
105
+ For a Workspace that stores no content, `harnessforce init --send-content` writes `0`, prints the reason, and
106
+ still exits 0. `harnessforce import` never sends content. `harnessforce run`
100
107
  never puts the user key or the API token in the agent's environment, arguments or settings. Keys and
101
108
  tokens are sent only to the origins pinned by `harnessforce init`.
102
109
 
103
110
  ## CLI credentials (`harnessforce init`, `harnessforce otel-headers`)
104
111
 
105
- `harnessforce init [--url <base URL>] [--port <port>] [--send-content]` connects this machine to a Harnessforce Workspace:
112
+ `harnessforce init [--port <port>] [--send-content] [--no-plugin-auto-update]` connects this machine to a Harnessforce Workspace:
106
113
 
107
114
  1. It checks the OS keychain first: macOS Keychain or the Windows Credential Manager (through
108
115
  [`@napi-rs/keyring`](https://github.com/Brooooooklyn/keyring-node)). On Linux it writes and deletes
@@ -131,23 +138,30 @@ tokens are sent only to the origins pinned by `harnessforce init`.
131
138
  5. It updates the Claude Code user settings (`~/.claude/settings.json`, or `$CLAUDE_CONFIG_DIR`),
132
139
  changing only these keys:
133
140
  - `env`: the OTel exporter variables, `HARNESSFORCE_URL`, `HARNESSFORCE_ENDPOINT` and
134
- `HARNESSFORCE_WORKSPACE_ID`, but no key. With `--send-content` and a Workspace that has opted in
135
- to content it also adds `OTEL_LOG_USER_PROMPTS=1`
141
+ `HARNESSFORCE_WORKSPACE_ID`, but no key, and the four `OTEL_LOG_*` content variables: `1` for a category
142
+ you send with `--send-content` and the Workspace stores, `0` otherwise
136
143
  - `otelHeadersHelper`: `harnessforce otel-headers`
137
144
  - `enabledPlugins["harnessforce@harnessforce-agent"]`
145
+ - `extraKnownMarketplaces["harnessforce-agent"]` (or `additionalMarketplaces` when only that alias is
146
+ present): the GitHub source and `autoUpdate: true`, so Claude Code updates the plugin in the
147
+ background. `--no-plugin-auto-update` writes `false`; a value you set yourself is kept.
138
148
  6. It updates the Codex `config.toml` (`$CODEX_HOME` when it is an absolute path, otherwise `~/.codex`),
139
- changing only these three items and keeping every other key and comment:
149
+ changing only these items and keeping every other key and comment:
140
150
  - `[otel].exporter`: OTLP/HTTP (`protocol = "binary"`) to the ingest endpoint with
141
151
  `Authorization = "Bearer <user ingest key>"`. Codex cannot take the header from a command or expand
142
152
  `${VAR}`, so this is the one place a key is written to a plain file. It is the user ingest key only
143
- (never the API token), and you can revoke it on the Workspace's send keys page. With `--send-content`
144
- and a Workspace that has opted in to content it also sets `log_user_prompt = true`
153
+ (never the API token), and you can revoke it on the Workspace's send keys page. `log_user_prompt` and
154
+ `log_agent_responses` are set to `true` for a category you send with `--send-content` and the
155
+ Workspace stores, and `false` otherwise
145
156
  - `[[hooks.SessionStart]]`: a handler that runs `harnessforce hook session-start`
157
+ - `[[hooks.PostToolUse]]`: a handler with `matcher = "^apply_patch$"` that runs `harnessforce hook post-tool-use`
158
+ asynchronously
159
+ - `[[hooks.Stop]]`: a handler that runs `harnessforce hook stop` asynchronously
146
160
  - `[mcp_servers.harnessforce]`: `url = "<base URL>/mcp"`
147
161
 
148
162
  The file is written through a temporary file in the same directory and renamed into place, with mode
149
- `0600`. Running `harnessforce init` again replaces only these three items. Restart Codex afterwards,
150
- trust the hook with `/hooks` (Codex runs an untrusted hook only after you review it), and run
163
+ `0600`. Running `harnessforce init` again replaces only these items. Restart Codex afterwards,
164
+ trust the three hooks with `/hooks` (Codex runs an untrusted hook only after you review it), and run
151
165
  `codex mcp login harnessforce` to use the MCP server.
152
166
 
153
167
  `harnessforce otel-headers` prints `{"Authorization":"Bearer <key>"}`. The key is `HARNESSFORCE_INGEST_KEY` from
@@ -169,16 +183,22 @@ authorization server metadata of the base URL, only when that endpoint is on the
169
183
  origin. The new pair replaces `<workspace_id>:api-token`. A lock older than 60 seconds is treated as
170
184
  abandoned, and waiting more than 30 seconds for it counts as a failed refresh. If the refresh fails, or
171
185
  the refreshed token is also answered with 401, they stop with
172
- 「ログインの有効期限が切れました。`harnessforce init`を実行してください」.
186
+ "Your login has expired. Run `harnessforce init`".
187
+
188
+ ## Updates
189
+
190
+ Every request to Harnessforce carries `User-Agent: harnessforce-cli/<version>` and nothing else about
191
+ the machine. The server answers with the update status, which is kept in
192
+ `~/.harnessforce/update-status.json`:
173
193
 
174
- ### Default connection
194
+ - A newer version is shown once a day after a command (not with `NO_UPDATE_NOTIFIER` or `CI`).
195
+ - After a security fix, the date this version stops sending is shown after every command.
196
+ - An unsupported version sends nothing: sending commands exit 1, `harnessforce run` starts the agent
197
+ without the Harnessforce environment, `harnessforce otel-headers` exits 5 without a key, and the Codex
198
+ hook registers nothing. After 24 hours it checks again with `GET /v1/client-status`, without
199
+ credentials.
175
200
 
176
- The default base URL is an optional build input: `HARNESSFORCE_BUILD_URL=<apps/web base URL> pnpm build`.
177
- Without it the default is `https://harnessforce.dev`. The value must be `https:`, or `http:` for localhost.
178
- At run time the base URL is the environment variable `HARNESSFORCE_URL`, then `env.HARNESSFORCE_URL` in the
179
- user settings (`harnessforce init --url` writes it), then the build default. The plugin build
180
- uses the same value for its MCP server URL. See
181
- [Building the local marketplace](https://github.com/lambda-script/harnessforce-agent/blob/main/docs/runbooks/local-marketplace.md).
201
+ Update with `npm install -g @harnessforce/cli@latest`. The CLI never replaces itself.
182
202
 
183
203
  ## Launching an agent for an Issue (`harnessforce run`)
184
204
 
@@ -236,6 +256,10 @@ work done before the plugin was installed can be linked to issues too. Run `harn
236
256
  - Sessions are sent 100 at a time to `POST <HARNESSFORCE_ENDPOINT>/v1/imports/sessions` with the user
237
257
  ingest key, only when the endpoint origin matches the one pinned by `harnessforce init`. `429` and `503` are
238
258
  retried up to 3 times after `Retry-After` (at most 60 seconds).
259
+ - Sessions that are not sent are counted by the first reason that applies (already sent, outside the
260
+ import window, outside a git repository, repository not connected to the Workspace) and printed on one line.
261
+ When nothing was imported because no repository is connected, it says to connect the repository
262
+ with the GitHub App in Harnessforce.
239
263
  - Sent sessions are recorded in `~/.harnessforce/import-state.json` per Workspace and ingest endpoint,
240
264
  so running it again continues where it stopped and never sends a session twice to the same place.
241
265
  Sessions dropped because of the monthly event limit or a read-only Workspace are not recorded and