@nessielabs/daemon 0.4.0 → 0.5.26
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 +8 -17
- package/dist/cli.js +42 -21
- package/dist/commands/auth.js +116 -0
- package/dist/commands/integrations.js +84 -0
- package/dist/commands/login.js +145 -0
- package/dist/commands/run.js +21 -6
- package/dist/commands/service.js +18 -36
- package/dist/commands/sharing.js +399 -0
- package/dist/commands/status.js +67 -0
- package/dist/commands/teams.js +64 -0
- package/dist/nera/client.js +156 -0
- package/dist/nera/process.js +78 -0
- package/dist/nera/runtimeFile.js +25 -0
- package/dist/platform/binary.js +21 -0
- package/dist/platform/openBrowser.js +14 -0
- package/dist/platform/paths.js +13 -0
- package/dist/platform/table.js +85 -0
- package/package.json +9 -5
- package/vendor/nera/darwin-arm64/nera +0 -0
- package/vendor/nera/darwin-x64/nera +0 -0
- package/vendor/nera/linux-arm64/nera +0 -0
- package/vendor/nera/linux-x64/nera +0 -0
- package/vendor/nera/win32-arm64/nera.exe +0 -0
- package/vendor/nera/win32-x64/nera.exe +0 -0
- package/vendor/proto/nessie.edge.v1.proto +475 -0
- package/dist/api/http.js +0 -44
- package/dist/auth/deviceFlow.js +0 -36
- package/dist/auth/tokens.js +0 -30
- package/dist/commands/setup.js +0 -75
- package/dist/config/endpoints.js +0 -2
- package/dist/config/paths.js +0 -19
- package/dist/config/store.js +0 -27
- package/dist/git/repoResolver.js +0 -215
- package/dist/localStore/daemonGraphStore.js +0 -494
- package/dist/localStore/messageComparison.js +0 -37
- package/dist/localStore/pushMapping.js +0 -69
- package/dist/localStore/rowTypes.js +0 -1
- package/dist/localStore/schema.js +0 -70
- package/dist/localStore/uuid.js +0 -12
- package/dist/parser/claudeCode.js +0 -202
- package/dist/parser/codex.js +0 -91
- package/dist/parser/jsonl.js +0 -24
- package/dist/parser/types.js +0 -15
- package/dist/redact/secrets.js +0 -60
- package/dist/service/process.js +0 -134
- package/dist/service/systemd.js +0 -38
- package/dist/sources/detect.js +0 -43
- package/dist/sync/batch.js +0 -50
- package/dist/sync/client.js +0 -90
- package/dist/sync/exclusions.js +0 -10
- package/dist/sync/hash.js +0 -4
- package/dist/sync/loop.js +0 -106
- package/dist/sync/scan.js +0 -245
- package/dist/sync/slices.js +0 -63
- package/dist/sync/types.js +0 -1
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package nessie.edge.v1;
|
|
4
|
+
|
|
5
|
+
service Nera {
|
|
6
|
+
rpc GetHealth(GetHealthRequest) returns (GetHealthResponse);
|
|
7
|
+
|
|
8
|
+
// Interactive sign-in (RFC 8252 loopback): nera binds a one-shot HTTP
|
|
9
|
+
// listener on 127.0.0.1 and returns a Google OAuth URL for the shell to
|
|
10
|
+
// open in a browser. The OAuth callback lands directly on the listener;
|
|
11
|
+
// tokens never transit the shell. A completed sign-in ADDS an account and
|
|
12
|
+
// makes it active. Watch WatchAuthState for the outcome.
|
|
13
|
+
rpc BeginSignIn(BeginSignInRequest) returns (BeginSignInResponse);
|
|
14
|
+
|
|
15
|
+
// Headless sign-in: device pairing approved from a signed-in Nessie app,
|
|
16
|
+
// or a static sk_nes_v1 API key. Nera polls the pairing endpoint
|
|
17
|
+
// internally; a redeemed pairing adds an account and makes it active.
|
|
18
|
+
// SetApiKey validates the key against the backend, resolves its identity,
|
|
19
|
+
// and adds it as a first-class account. Watch WatchAuthState for the
|
|
20
|
+
// outcome.
|
|
21
|
+
rpc StartDevicePairing(StartDevicePairingRequest) returns (DevicePairingInfo);
|
|
22
|
+
rpc SetApiKey(SetApiKeyRequest) returns (AuthStateResponse);
|
|
23
|
+
|
|
24
|
+
// Makes a stored account the active one. Fails with NotFound for an
|
|
25
|
+
// unknown account id.
|
|
26
|
+
rpc SwitchAccount(SwitchAccountRequest) returns (AuthStateResponse);
|
|
27
|
+
|
|
28
|
+
// Removes one account's credential and cached identity. Defaults to the
|
|
29
|
+
// active account; its database file is closed but kept on disk. If other
|
|
30
|
+
// accounts remain, the most recently used one becomes active.
|
|
31
|
+
rpc SignOut(SignOutRequest) returns (AuthStateResponse);
|
|
32
|
+
|
|
33
|
+
rpc GetAuthState(GetAuthStateRequest) returns (AuthStateResponse);
|
|
34
|
+
rpc WatchAuthState(WatchAuthStateRequest) returns (stream AuthStateResponse);
|
|
35
|
+
|
|
36
|
+
// Team membership for the ACTIVE account. These proxy the backend with
|
|
37
|
+
// nera's stored credential — no team state is cached locally; the backend
|
|
38
|
+
// stays the source of truth. Membership management (creating teams,
|
|
39
|
+
// inviting others) is out of scope for this surface.
|
|
40
|
+
rpc ListTeams(ListTeamsRequest) returns (ListTeamsResponse);
|
|
41
|
+
rpc ListTeamInvites(ListTeamInvitesRequest) returns (ListTeamInvitesResponse);
|
|
42
|
+
rpc RespondToTeamInvite(RespondToTeamInviteRequest) returns (RespondToTeamInviteResponse);
|
|
43
|
+
rpc ListTeamSharingPolicies(ListTeamSharingPoliciesRequest) returns (ListTeamSharingPoliciesResponse);
|
|
44
|
+
|
|
45
|
+
// Team sharing (ACL) for one node, proxied to the backend like the team
|
|
46
|
+
// RPCs — the backend stays the source of truth, nothing is cached
|
|
47
|
+
// locally. GetNodeSharing reads the node's full grant list; ShareNode
|
|
48
|
+
// and UnshareNode mutate it and return the fresh state. All three
|
|
49
|
+
// require an OAuth-backed active account.
|
|
50
|
+
rpc GetNodeSharing(GetNodeSharingRequest) returns (NodeSharing);
|
|
51
|
+
rpc ShareNode(ShareNodeRequest) returns (NodeSharing);
|
|
52
|
+
rpc UnshareNode(UnshareNodeRequest) returns (NodeSharing);
|
|
53
|
+
|
|
54
|
+
// Capture sources for the ACTIVE account, stored in its database's
|
|
55
|
+
// integration table (a strict subset of the Mac app's model). Discovery
|
|
56
|
+
// is a read-only scan; nothing is captured until a source is explicitly
|
|
57
|
+
// added AND the ingestion engine runs it (per-source `enabled` gates
|
|
58
|
+
// membership in the automatic loop).
|
|
59
|
+
rpc DiscoverSources(DiscoverSourcesRequest) returns (DiscoverSourcesResponse);
|
|
60
|
+
rpc AddSource(AddSourceRequest) returns (Source);
|
|
61
|
+
rpc ListSources(ListSourcesRequest) returns (ListSourcesResponse);
|
|
62
|
+
rpc ConfigureSource(ConfigureSourceRequest) returns (Source);
|
|
63
|
+
// Mac-app removal semantics: cancel sync, clear team ACLs (aborting the
|
|
64
|
+
// removal if that fails), tombstone the source's nodes and its
|
|
65
|
+
// integration root, then soft-delete the integration row. Tombstones
|
|
66
|
+
// propagate to the cloud on the next sync cycle.
|
|
67
|
+
rpc RemoveSource(RemoveSourceRequest) returns (RemoveSourceResponse);
|
|
68
|
+
|
|
69
|
+
// Engine control. Start/Stop toggle the PERSISTED automatic schedules
|
|
70
|
+
// (per account); Trigger runs one pass immediately regardless of the
|
|
71
|
+
// automatic state. Ingestion scans enabled sources into the local
|
|
72
|
+
// database; cloud sync runs one preflight -> pull -> push cycle against
|
|
73
|
+
// the cloud with the active account's credential.
|
|
74
|
+
rpc StartIngestion(StartIngestionRequest) returns (StartIngestionResponse);
|
|
75
|
+
rpc StopIngestion(StopIngestionRequest) returns (StopIngestionResponse);
|
|
76
|
+
rpc TriggerIngestion(TriggerIngestionRequest) returns (TriggerIngestionResponse);
|
|
77
|
+
rpc StartCloudSync(StartCloudSyncRequest) returns (StartCloudSyncResponse);
|
|
78
|
+
rpc StopCloudSync(StopCloudSyncRequest) returns (StopCloudSyncResponse);
|
|
79
|
+
rpc TriggerCloudSync(TriggerCloudSyncRequest) returns (TriggerCloudSyncResponse);
|
|
80
|
+
rpc GetRuntimeStatus(GetRuntimeStatusRequest) returns (RuntimeStatusResponse);
|
|
81
|
+
|
|
82
|
+
// Account-level cloud sync opt-in, distinct from the Start/Stop loop
|
|
83
|
+
// toggles above (which require the account to already be opted in).
|
|
84
|
+
// The backend's CloudSyncEnabled flag is latched by the first sync
|
|
85
|
+
// preflight, so Enable runs one preflight explicitly — that IS the
|
|
86
|
+
// opt-in — then re-reads the user and turns the automatic loop on.
|
|
87
|
+
// Disable turns the automatic loop off; matching the Mac app, it does
|
|
88
|
+
// not clear the account-level flag on the backend (other devices keep
|
|
89
|
+
// their own sync state). Both return the refreshed auth state so the
|
|
90
|
+
// caller sees cloud_sync_enabled without a follow-up GetAuthState.
|
|
91
|
+
rpc EnableCloudSync(EnableCloudSyncRequest) returns (AuthStateResponse);
|
|
92
|
+
rpc DisableCloudSync(DisableCloudSyncRequest) returns (AuthStateResponse);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
message GetHealthRequest {}
|
|
96
|
+
|
|
97
|
+
message GetHealthResponse {
|
|
98
|
+
string status = 1;
|
|
99
|
+
string version = 2;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
message BeginSignInRequest {}
|
|
103
|
+
|
|
104
|
+
message BeginSignInResponse {
|
|
105
|
+
// Google OAuth URL to open in the user's browser.
|
|
106
|
+
string url = 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
message SwitchAccountRequest {
|
|
110
|
+
// Account (user) id to activate.
|
|
111
|
+
string user_id = 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
message SignOutRequest {
|
|
115
|
+
// Account (user) id to sign out; empty means the active account.
|
|
116
|
+
string user_id = 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
message StartDevicePairingRequest {
|
|
120
|
+
// Human-readable name shown in the approving app, e.g. a hostname.
|
|
121
|
+
string device_name = 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
message DevicePairingInfo {
|
|
125
|
+
// Code the user enters in an already-signed-in Nessie app to approve.
|
|
126
|
+
string user_code = 1;
|
|
127
|
+
string verification_hint = 2;
|
|
128
|
+
// RFC 3339; the pairing must be approved before this instant.
|
|
129
|
+
string expires_at = 3;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
message SetApiKeyRequest {
|
|
133
|
+
string api_key = 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
message GetAuthStateRequest {}
|
|
137
|
+
|
|
138
|
+
message WatchAuthStateRequest {}
|
|
139
|
+
|
|
140
|
+
enum AuthStatus {
|
|
141
|
+
AUTH_STATUS_UNSPECIFIED = 0;
|
|
142
|
+
AUTH_STATUS_SIGNED_OUT = 1;
|
|
143
|
+
// A device pairing is awaiting approval; pairing_user_code is set.
|
|
144
|
+
AUTH_STATUS_PAIRING_PENDING = 2;
|
|
145
|
+
AUTH_STATUS_SIGNED_IN = 3;
|
|
146
|
+
// The active account's credentials were rejected and cannot be
|
|
147
|
+
// refreshed; a new sign-in is required for that account.
|
|
148
|
+
AUTH_STATUS_REAUTH_REQUIRED = 4;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
message AuthUser {
|
|
152
|
+
string id = 1;
|
|
153
|
+
string email = 2;
|
|
154
|
+
string name = 3;
|
|
155
|
+
bool cloud_sync_enabled = 4;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
message ListTeamsRequest {}
|
|
159
|
+
|
|
160
|
+
message Team {
|
|
161
|
+
string team_id = 1;
|
|
162
|
+
string name = 2;
|
|
163
|
+
// The active account's role in the team: "admin" or "member".
|
|
164
|
+
string role = 3;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
message ListTeamsResponse {
|
|
168
|
+
repeated Team teams = 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
message ListTeamInvitesRequest {}
|
|
172
|
+
|
|
173
|
+
message TeamInvite {
|
|
174
|
+
string team_id = 1;
|
|
175
|
+
string team_name = 2;
|
|
176
|
+
// Display name of the inviter, e.g. for "Erik invited you to Parrot".
|
|
177
|
+
string invited_by_name = 3;
|
|
178
|
+
// RFC 3339.
|
|
179
|
+
string created_at = 4;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
message ListTeamInvitesResponse {
|
|
183
|
+
repeated TeamInvite invites = 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
message RespondToTeamInviteRequest {
|
|
187
|
+
string team_id = 1;
|
|
188
|
+
// false declines the invite.
|
|
189
|
+
bool accept = 2;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
message RespondToTeamInviteResponse {
|
|
193
|
+
// The joined team; unset when the invite was declined.
|
|
194
|
+
Team team = 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
message ListTeamSharingPoliciesRequest {
|
|
198
|
+
string team_id = 1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
message TeamSharingPolicy {
|
|
202
|
+
string id = 1;
|
|
203
|
+
string team_id = 2;
|
|
204
|
+
string name = 3;
|
|
205
|
+
repeated string resource_kinds = 4;
|
|
206
|
+
string share_scope = 5;
|
|
207
|
+
repeated string repo_keys = 6;
|
|
208
|
+
string audience = 7;
|
|
209
|
+
repeated string audience_user_ids = 8;
|
|
210
|
+
bool enabled = 9;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
message ListTeamSharingPoliciesResponse {
|
|
214
|
+
repeated TeamSharingPolicy policies = 1;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
message AuthStateResponse {
|
|
218
|
+
// Status of the ACTIVE account (SIGNED_OUT when no account is active).
|
|
219
|
+
AuthStatus status = 1;
|
|
220
|
+
// The active account's identity; set when status is SIGNED_IN or
|
|
221
|
+
// REAUTH_REQUIRED.
|
|
222
|
+
AuthUser user = 2;
|
|
223
|
+
// Set while a device pairing is pending, even when an account is active
|
|
224
|
+
// (status only becomes PAIRING_PENDING when no account is).
|
|
225
|
+
string pairing_user_code = 3;
|
|
226
|
+
// Every stored account, active one included.
|
|
227
|
+
repeated AuthUser accounts = 4;
|
|
228
|
+
// Last known billing status for the ACTIVE account. Unset when no
|
|
229
|
+
// status has been fetched yet (freshly signed in, offline) — unset
|
|
230
|
+
// means "unknown", never "no access". Refreshed on auth changes and at
|
|
231
|
+
// the start of every cloud sync cycle.
|
|
232
|
+
BillingState billing = 5;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// The active account's subscription state, read from the backend's
|
|
236
|
+
// GET /billing/status (never initiated or computed locally when the
|
|
237
|
+
// backend supplies entitlements). nera enforces it: no access pauses
|
|
238
|
+
// ingestion and cloud sync; the integration limit caps enabled sources.
|
|
239
|
+
// Auth and team RPCs are never billing-gated — joining a team is often
|
|
240
|
+
// how an account gains a subscription.
|
|
241
|
+
message BillingState {
|
|
242
|
+
// Backend tier vocabulary: "none", "plus", "plus_v2", "pro", "team",
|
|
243
|
+
// "grandfathered_*".
|
|
244
|
+
string tier = 1;
|
|
245
|
+
// Stripe subscription status: "active", "trialing", "past_due", ...
|
|
246
|
+
string status = 2;
|
|
247
|
+
// Product access. false pauses ingestion and cloud sync.
|
|
248
|
+
bool has_access = 3;
|
|
249
|
+
// CLI/MCP access.
|
|
250
|
+
bool has_mcp_access = 4;
|
|
251
|
+
// Max ACTIVE integrations; 0 = unlimited.
|
|
252
|
+
uint32 active_integration_limit = 5;
|
|
253
|
+
// "personal_subscription" | "team_subscription" | "grandfathered" |
|
|
254
|
+
// "none".
|
|
255
|
+
string access_source = 6;
|
|
256
|
+
// Set when access is inherited from a team subscription.
|
|
257
|
+
string team_name = 7;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// --- Node sharing ---
|
|
261
|
+
|
|
262
|
+
message GetNodeSharingRequest {
|
|
263
|
+
// Node id (UUID) whose sharing state to read.
|
|
264
|
+
string node_id = 1;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Who a grant covers within its team.
|
|
268
|
+
enum Audience {
|
|
269
|
+
AUDIENCE_UNSPECIFIED = 0;
|
|
270
|
+
// Everyone in the team.
|
|
271
|
+
AUDIENCE_TEAM = 1;
|
|
272
|
+
// Team admins only.
|
|
273
|
+
AUDIENCE_ADMINS = 2;
|
|
274
|
+
// One specific member (user_id).
|
|
275
|
+
AUDIENCE_USER = 3;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// How much of the node's data a grant covers.
|
|
279
|
+
enum Scope {
|
|
280
|
+
SCOPE_UNSPECIFIED = 0;
|
|
281
|
+
// Everything under the node.
|
|
282
|
+
SCOPE_ALL = 1;
|
|
283
|
+
// Only the listed repo_keys.
|
|
284
|
+
SCOPE_REPOS = 2;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// One ACL grant covering the node: either the node's own grant or one
|
|
288
|
+
// inherited from an ancestor (see `inherited`).
|
|
289
|
+
message SharingGrant {
|
|
290
|
+
string team_id = 1;
|
|
291
|
+
Audience audience = 2;
|
|
292
|
+
// Set when audience is AUDIENCE_USER.
|
|
293
|
+
string user_id = 3;
|
|
294
|
+
Scope scope = 4;
|
|
295
|
+
// Set when scope is SCOPE_REPOS.
|
|
296
|
+
repeated string repo_keys = 5;
|
|
297
|
+
// True when the grant lives on an ancestor's ACL, not this node's.
|
|
298
|
+
// Mac-parity semantics: the local `contains` chain is walked
|
|
299
|
+
// closest-first, and for each team only the CLOSEST shared ancestor
|
|
300
|
+
// contributes grants; an inherited grant whose (team, audience, user)
|
|
301
|
+
// exactly matches one of the node's own grants is dropped (the own
|
|
302
|
+
// grant wins). Inherited grants are read-only through this API —
|
|
303
|
+
// ShareNode/UnshareNode operate on the node's own ACL only, so to
|
|
304
|
+
// remove one, unshare the ancestor it came from (the Mac disables
|
|
305
|
+
// removal of inherited rows the same way).
|
|
306
|
+
bool inherited = 6;
|
|
307
|
+
// The ancestor node whose ACL carries the grant; empty on own grants.
|
|
308
|
+
string inherited_from_node_id = 7;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
message NodeSharing {
|
|
312
|
+
// Canonical (lowercase) node id.
|
|
313
|
+
string node_id = 1;
|
|
314
|
+
repeated SharingGrant grants = 2;
|
|
315
|
+
// Distinct repo keys among the node's live children in the local store,
|
|
316
|
+
// sorted — lets a share sheet offer repo-scoped choices without another
|
|
317
|
+
// RPC.
|
|
318
|
+
repeated string available_repo_keys = 3;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
message ShareNodeRequest {
|
|
322
|
+
string node_id = 1;
|
|
323
|
+
string team_id = 2;
|
|
324
|
+
// AUDIENCE_TEAM and AUDIENCE_ADMINS share one backend row per
|
|
325
|
+
// (team, node) — the backend key has no admin component — so sharing
|
|
326
|
+
// one over the other replaces it rather than adding a second grant
|
|
327
|
+
// (Mac parity: selecting Everyone displaces Admins and vice versa).
|
|
328
|
+
Audience audience = 3;
|
|
329
|
+
// AUDIENCE_USER only: one grant is written per listed member.
|
|
330
|
+
repeated string user_ids = 4;
|
|
331
|
+
Scope scope = 5;
|
|
332
|
+
// Required iff scope is SCOPE_REPOS.
|
|
333
|
+
repeated string repo_keys = 6;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Selectors address the NODE'S OWN ACL only. Grants inherited from an
|
|
337
|
+
// ancestor are never touched: a selector that only matches inherited
|
|
338
|
+
// grants is a no-op (the deletes are keyed on this node's id, which has
|
|
339
|
+
// no such rows). This mirrors the Mac, whose share modal disables the
|
|
340
|
+
// remove action on inherited rows without calling the server.
|
|
341
|
+
message UnshareNodeRequest {
|
|
342
|
+
string node_id = 1;
|
|
343
|
+
// Empty (with empty user_ids): remove every grant the caller owns for
|
|
344
|
+
// the node, across all teams. Set alone: remove that team's grants
|
|
345
|
+
// (team-wide and per-user).
|
|
346
|
+
string team_id = 2;
|
|
347
|
+
// Remove only these members' per-user grants; team_id is required.
|
|
348
|
+
repeated string user_ids = 3;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// --- Capture sources ---
|
|
352
|
+
|
|
353
|
+
enum SourceState {
|
|
354
|
+
SOURCE_STATE_UNSPECIFIED = 0;
|
|
355
|
+
// Detected / configured; will be scanned when enabled.
|
|
356
|
+
SOURCE_STATE_READY = 1;
|
|
357
|
+
// The source's base path does not exist on this machine.
|
|
358
|
+
SOURCE_STATE_NOT_DETECTED = 2;
|
|
359
|
+
// Account-backed source needs a sign-in (future kinds; unused for
|
|
360
|
+
// local coding agents).
|
|
361
|
+
SOURCE_STATE_NEEDS_AUTH = 3;
|
|
362
|
+
SOURCE_STATE_ERROR = 4;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
message Source {
|
|
366
|
+
// Integration row id (UUID).
|
|
367
|
+
string source_id = 1;
|
|
368
|
+
// "codex" | "claude_code" | "cursor" | "claude_cowork" | "pi".
|
|
369
|
+
string kind = 2;
|
|
370
|
+
// e.g. "Claude Code on tigers-mbp".
|
|
371
|
+
string display_name = 3;
|
|
372
|
+
// Member of the automatic ingestion loop.
|
|
373
|
+
bool enabled = 4;
|
|
374
|
+
SourceState state = 5;
|
|
375
|
+
string base_path = 6;
|
|
376
|
+
// Connected account for account-backed kinds; empty for local agents.
|
|
377
|
+
string account_hint = 7;
|
|
378
|
+
// RFC 3339 instant of the last successful ingestion pass; empty if never.
|
|
379
|
+
string last_synced_at = 8;
|
|
380
|
+
// Detail for SOURCE_STATE_ERROR; empty when healthy.
|
|
381
|
+
string last_error = 9;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
message DiscoverSourcesRequest {}
|
|
385
|
+
|
|
386
|
+
message DiscoveredSource {
|
|
387
|
+
string kind = 1;
|
|
388
|
+
string display_name = 2;
|
|
389
|
+
string base_path = 3;
|
|
390
|
+
// An instance of this kind is already added for the active account.
|
|
391
|
+
bool already_added = 4;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
message DiscoverSourcesResponse {
|
|
395
|
+
repeated DiscoveredSource sources = 1;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
message AddSourceRequest {
|
|
399
|
+
string kind = 1;
|
|
400
|
+
// Override the default base path for the kind.
|
|
401
|
+
string base_path = 2;
|
|
402
|
+
// false registers the source without entering the automatic loop (the
|
|
403
|
+
// onboarding pattern: add during consent, start capture later).
|
|
404
|
+
bool enabled = 3;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
message ListSourcesRequest {}
|
|
408
|
+
|
|
409
|
+
message ListSourcesResponse {
|
|
410
|
+
repeated Source sources = 1;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
message ConfigureSourceRequest {
|
|
414
|
+
string source_id = 1;
|
|
415
|
+
optional bool enabled = 2;
|
|
416
|
+
optional string base_path = 3;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
message RemoveSourceRequest {
|
|
420
|
+
string source_id = 1;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
message RemoveSourceResponse {}
|
|
424
|
+
|
|
425
|
+
// --- Engine control ---
|
|
426
|
+
|
|
427
|
+
message StartIngestionRequest {}
|
|
428
|
+
message StartIngestionResponse {}
|
|
429
|
+
message StopIngestionRequest {}
|
|
430
|
+
message StopIngestionResponse {}
|
|
431
|
+
|
|
432
|
+
message TriggerIngestionRequest {}
|
|
433
|
+
message TriggerIngestionResponse {
|
|
434
|
+
// Enabled sources the pass covers.
|
|
435
|
+
repeated string source_ids = 1;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
message StartCloudSyncRequest {}
|
|
439
|
+
message StartCloudSyncResponse {}
|
|
440
|
+
message StopCloudSyncRequest {}
|
|
441
|
+
message StopCloudSyncResponse {}
|
|
442
|
+
|
|
443
|
+
message TriggerCloudSyncRequest {}
|
|
444
|
+
message TriggerCloudSyncResponse {}
|
|
445
|
+
|
|
446
|
+
message GetRuntimeStatusRequest {}
|
|
447
|
+
|
|
448
|
+
message EngineStatus {
|
|
449
|
+
// Persisted automatic schedule state for the active account.
|
|
450
|
+
bool auto_enabled = 1;
|
|
451
|
+
// True while a pass/cycle is currently executing.
|
|
452
|
+
bool running = 2;
|
|
453
|
+
// "manual", "timer", or "dirty_nudge" for the last started pass/cycle.
|
|
454
|
+
string last_trigger = 3;
|
|
455
|
+
// RFC 3339 instant for the last started pass/cycle; empty if unknown.
|
|
456
|
+
string last_started_at = 4;
|
|
457
|
+
// RFC 3339 instant for the last completed pass/cycle; empty if unknown.
|
|
458
|
+
string last_completed_at = 5;
|
|
459
|
+
// RFC 3339 instant for the last successful pass/cycle; empty if unknown.
|
|
460
|
+
string last_success_at = 6;
|
|
461
|
+
// Last observed failure; empty when the last pass/cycle succeeded or none ran.
|
|
462
|
+
string last_error = 7;
|
|
463
|
+
// RFC 3339 instant for the last skipped pass/cycle; empty if none skipped.
|
|
464
|
+
string last_skipped_at = 8;
|
|
465
|
+
// Reason the last pass/cycle was skipped; empty if none skipped.
|
|
466
|
+
string last_skip_reason = 9;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
message RuntimeStatusResponse {
|
|
470
|
+
EngineStatus ingestion = 1;
|
|
471
|
+
EngineStatus cloud_sync = 2;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
message EnableCloudSyncRequest {}
|
|
475
|
+
message DisableCloudSyncRequest {}
|
package/dist/api/http.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
export class ApiError extends Error {
|
|
2
|
-
status;
|
|
3
|
-
body;
|
|
4
|
-
constructor(message, status, body) {
|
|
5
|
-
super(message);
|
|
6
|
-
this.status = status;
|
|
7
|
-
this.body = body;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
export async function postJson(baseUrl, path, body, options = {}) {
|
|
11
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
12
|
-
const response = await fetchImpl(new URL(path, baseUrl), {
|
|
13
|
-
method: "POST",
|
|
14
|
-
headers: {
|
|
15
|
-
"content-type": "application/json",
|
|
16
|
-
...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
|
|
17
|
-
},
|
|
18
|
-
body: JSON.stringify(body),
|
|
19
|
-
});
|
|
20
|
-
const text = await response.text();
|
|
21
|
-
if (!response.ok) {
|
|
22
|
-
throw new ApiError(`HTTP status ${response.status}: ${text || response.statusText}`, response.status, text);
|
|
23
|
-
}
|
|
24
|
-
return (text ? JSON.parse(text) : {});
|
|
25
|
-
}
|
|
26
|
-
export async function getJson(baseUrl, path, options = {}) {
|
|
27
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
28
|
-
const url = new URL(path, baseUrl);
|
|
29
|
-
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
30
|
-
if (value !== undefined)
|
|
31
|
-
url.searchParams.set(key, value);
|
|
32
|
-
}
|
|
33
|
-
const response = await fetchImpl(url, {
|
|
34
|
-
method: "GET",
|
|
35
|
-
headers: {
|
|
36
|
-
...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
const text = await response.text();
|
|
40
|
-
if (!response.ok) {
|
|
41
|
-
throw new ApiError(`HTTP status ${response.status}: ${text || response.statusText}`, response.status, text);
|
|
42
|
-
}
|
|
43
|
-
return (text ? JSON.parse(text) : {});
|
|
44
|
-
}
|
package/dist/auth/deviceFlow.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
-
import { postJson } from "../api/http.js";
|
|
3
|
-
import { authApiUrl } from "../config/endpoints.js";
|
|
4
|
-
export async function runDeviceAuth(input) {
|
|
5
|
-
const log = input.log ?? console;
|
|
6
|
-
const fetchImpl = input.fetchImpl ?? fetch;
|
|
7
|
-
const sleepMs = input.sleepMs ?? sleep;
|
|
8
|
-
const start = await postJson(authApiUrl, "/auth/device/start", {
|
|
9
|
-
device_name: input.deviceName,
|
|
10
|
-
platform: "linux",
|
|
11
|
-
}, { fetchImpl });
|
|
12
|
-
log.log(`Open ${start.verification_uri_complete ?? start.verification_uri ?? "Nessie"} and enter code ${start.user_code}.`);
|
|
13
|
-
const intervalMs = Math.max(1, start.interval ?? 5) * 1000;
|
|
14
|
-
const expiresAt = Date.now() + Math.max(60, start.expires_in ?? 600) * 1000;
|
|
15
|
-
while (Date.now() < expiresAt) {
|
|
16
|
-
await sleepMs(intervalMs);
|
|
17
|
-
const poll = await postJson(authApiUrl, "/auth/device/poll", {
|
|
18
|
-
device_code: start.device_code,
|
|
19
|
-
}, { fetchImpl });
|
|
20
|
-
if (poll.status === "pending")
|
|
21
|
-
continue;
|
|
22
|
-
if (poll.status === "approved" && poll.access_token && poll.device_id) {
|
|
23
|
-
return {
|
|
24
|
-
accessToken: poll.access_token,
|
|
25
|
-
refreshToken: poll.refresh_token ?? null,
|
|
26
|
-
deviceId: poll.device_id,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
if (poll.status === "approved") {
|
|
30
|
-
throw new Error("Auth approved but server response missing access_token or device_id.");
|
|
31
|
-
}
|
|
32
|
-
if (poll.status === "expired")
|
|
33
|
-
break;
|
|
34
|
-
}
|
|
35
|
-
throw new Error("Device auth expired before approval.");
|
|
36
|
-
}
|
package/dist/auth/tokens.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { ApiError, postJson } from "../api/http.js";
|
|
2
|
-
import { authApiUrl } from "../config/endpoints.js";
|
|
3
|
-
export class AuthRefreshError extends Error {
|
|
4
|
-
}
|
|
5
|
-
export async function refreshAccessToken(config, options = {}) {
|
|
6
|
-
if (!config.refreshToken) {
|
|
7
|
-
throw new AuthRefreshError("Access token expired and no refresh token is configured.");
|
|
8
|
-
}
|
|
9
|
-
try {
|
|
10
|
-
const response = await postJson(authApiUrl, "/auth/refresh", {
|
|
11
|
-
refresh_token: config.refreshToken,
|
|
12
|
-
}, { fetchImpl: options.fetchImpl });
|
|
13
|
-
if (!response.access_token) {
|
|
14
|
-
throw new AuthRefreshError("Token refresh response missing access_token.");
|
|
15
|
-
}
|
|
16
|
-
return {
|
|
17
|
-
...config,
|
|
18
|
-
accessToken: response.access_token,
|
|
19
|
-
refreshToken: response.refresh_token ?? config.refreshToken,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
catch (error) {
|
|
23
|
-
if (error instanceof AuthRefreshError)
|
|
24
|
-
throw error;
|
|
25
|
-
if (error instanceof ApiError && error.status === 401) {
|
|
26
|
-
throw new AuthRefreshError("Refresh token is invalid or expired.");
|
|
27
|
-
}
|
|
28
|
-
throw error;
|
|
29
|
-
}
|
|
30
|
-
}
|
package/dist/commands/setup.js
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { checkbox, confirm } from "@inquirer/prompts";
|
|
2
|
-
import { hostname } from "node:os";
|
|
3
|
-
import { runDeviceAuth } from "../auth/deviceFlow.js";
|
|
4
|
-
import { getDaemonPaths } from "../config/paths.js";
|
|
5
|
-
import { createConfig, writeConfig } from "../config/store.js";
|
|
6
|
-
import { installUserService } from "../service/systemd.js";
|
|
7
|
-
import { detectSources } from "../sources/detect.js";
|
|
8
|
-
export async function runSetup(options, depsOverride = {}) {
|
|
9
|
-
const deps = resolveSetupDeps(depsOverride);
|
|
10
|
-
const deviceName = hostname();
|
|
11
|
-
if (options.apiKey !== undefined) {
|
|
12
|
-
await runAPIKeySetup({
|
|
13
|
-
apiKey: options.apiKey,
|
|
14
|
-
deviceName,
|
|
15
|
-
deps,
|
|
16
|
-
});
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
const auth = await runDeviceAuth({ deviceName });
|
|
20
|
-
const detectedSources = await deps.detectSources();
|
|
21
|
-
if (detectedSources.length === 0) {
|
|
22
|
-
throw new Error("No supported Codex or Claude Code history directories were found.");
|
|
23
|
-
}
|
|
24
|
-
const selectedSources = await checkbox({
|
|
25
|
-
message: "Select sources for this daemon to ingest",
|
|
26
|
-
choices: detectedSources.map((source) => ({
|
|
27
|
-
name: `${source.displayName} (${source.basePath})`,
|
|
28
|
-
value: source,
|
|
29
|
-
checked: true,
|
|
30
|
-
})),
|
|
31
|
-
required: true,
|
|
32
|
-
});
|
|
33
|
-
const config = createConfig({
|
|
34
|
-
accessToken: auth.accessToken,
|
|
35
|
-
refreshToken: auth.refreshToken,
|
|
36
|
-
deviceId: auth.deviceId,
|
|
37
|
-
deviceName,
|
|
38
|
-
selectedSources,
|
|
39
|
-
});
|
|
40
|
-
await deps.writeConfig(config, deps.paths);
|
|
41
|
-
console.log(`Saved config to ${deps.paths.configFile}`);
|
|
42
|
-
if (await confirm({ message: "Install and start the user systemd service?", default: true })) {
|
|
43
|
-
await installUserService(deps.paths);
|
|
44
|
-
console.log("Installed and started nessie-daemon.service");
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
async function runAPIKeySetup(input) {
|
|
48
|
-
const apiKey = input.apiKey.trim();
|
|
49
|
-
if (!apiKey)
|
|
50
|
-
throw new Error("--api-key cannot be empty.");
|
|
51
|
-
const detectedSources = await input.deps.detectSources();
|
|
52
|
-
if (detectedSources.length === 0) {
|
|
53
|
-
throw new Error("No supported Codex or Claude Code history directories were found.");
|
|
54
|
-
}
|
|
55
|
-
const config = createConfig({
|
|
56
|
-
apiKey,
|
|
57
|
-
deviceName: input.deviceName,
|
|
58
|
-
selectedSources: detectedSources,
|
|
59
|
-
});
|
|
60
|
-
await input.deps.writeConfig(config, input.deps.paths);
|
|
61
|
-
console.log("Configured Nessie daemon with API key auth.");
|
|
62
|
-
console.log(`Selected ${detectedSources.length} source(s):`);
|
|
63
|
-
for (const source of detectedSources) {
|
|
64
|
-
console.log(`- ${source.displayName} (${source.basePath})`);
|
|
65
|
-
}
|
|
66
|
-
console.log(`Saved config to ${input.deps.paths.configFile}`);
|
|
67
|
-
console.log("Run `nessie-daemon start` to start syncing.");
|
|
68
|
-
}
|
|
69
|
-
function resolveSetupDeps(overrides) {
|
|
70
|
-
return {
|
|
71
|
-
detectSources: overrides.detectSources ?? detectSources,
|
|
72
|
-
writeConfig: overrides.writeConfig ?? writeConfig,
|
|
73
|
-
paths: overrides.paths ?? getDaemonPaths(),
|
|
74
|
-
};
|
|
75
|
-
}
|
package/dist/config/endpoints.js
DELETED
package/dist/config/paths.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { homedir } from "node:os";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
export function getDaemonPaths(env = process.env, home = homedir()) {
|
|
4
|
-
const configHome = env.XDG_CONFIG_HOME ?? join(home, ".config");
|
|
5
|
-
const stateHome = env.XDG_STATE_HOME ?? join(home, ".local", "state");
|
|
6
|
-
const configDir = join(configHome, "nessie-daemon");
|
|
7
|
-
const stateDir = join(stateHome, "nessie-daemon");
|
|
8
|
-
const systemdUserDir = join(configHome, "systemd", "user");
|
|
9
|
-
return {
|
|
10
|
-
configDir,
|
|
11
|
-
stateDir,
|
|
12
|
-
configFile: join(configDir, "config.json"),
|
|
13
|
-
databaseFile: join(stateDir, "nessie.sqlite"),
|
|
14
|
-
pidFile: join(stateDir, "daemon.pid"),
|
|
15
|
-
logFile: join(stateDir, "daemon.log"),
|
|
16
|
-
systemdUserDir,
|
|
17
|
-
systemdServiceFile: join(systemdUserDir, "nessie-daemon.service"),
|
|
18
|
-
};
|
|
19
|
-
}
|