@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.
- package/README.md +63 -39
- package/dist/bin.js +1648 -1190
- 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 [--
|
|
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
|
|
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
|
|
77
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 [--
|
|
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
|
|
135
|
-
|
|
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
|
|
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.
|
|
144
|
-
|
|
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
|
|
150
|
-
trust the
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|