@indigoai-us/hq-cli 5.108.0 → 5.108.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 +79 -0
- package/assets/mesh-daemon/README.md +23 -0
- package/assets/mesh-daemon/launchd.plist.template +33 -0
- package/assets/mesh-daemon/systemd.system.service.template +27 -0
- package/assets/mesh-daemon/systemd.user.service.template +18 -0
- package/assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh +267 -0
- package/dist/commands/cloud.d.ts +6 -0
- package/dist/commands/cloud.js +26 -3
- package/dist/commands/core.js +5 -4
- package/dist/commands/mesh.d.ts +15 -0
- package/dist/commands/mesh.js +732 -4
- package/dist/commands/search.d.ts +23 -0
- package/dist/commands/search.js +42 -1
- package/dist/lib/doctor/checks/work-context.d.ts +23 -0
- package/dist/lib/doctor/checks/work-context.js +195 -0
- package/dist/lib/doctor/registry.js +4 -0
- package/dist/lib/mesh/api.d.ts +18 -0
- package/dist/lib/mesh/api.js +55 -0
- package/dist/lib/mesh/cache.js +35 -4
- package/dist/lib/mesh/client.d.ts +118 -0
- package/dist/lib/mesh/client.js +268 -0
- package/dist/lib/mesh/live/backoff.d.ts +16 -0
- package/dist/lib/mesh/live/backoff.js +20 -0
- package/dist/lib/mesh/live/daemon/board-refresh.d.ts +49 -0
- package/dist/lib/mesh/live/daemon/board-refresh.js +185 -0
- package/dist/lib/mesh/live/daemon/credentials.d.ts +71 -0
- package/dist/lib/mesh/live/daemon/credentials.js +227 -0
- package/dist/lib/mesh/live/daemon/doctor.d.ts +37 -0
- package/dist/lib/mesh/live/daemon/doctor.js +139 -0
- package/dist/lib/mesh/live/daemon/index.d.ts +24 -0
- package/dist/lib/mesh/live/daemon/index.js +14 -0
- package/dist/lib/mesh/live/daemon/install.d.ts +83 -0
- package/dist/lib/mesh/live/daemon/install.js +330 -0
- package/dist/lib/mesh/live/daemon/log.d.ts +11 -0
- package/dist/lib/mesh/live/daemon/log.js +79 -0
- package/dist/lib/mesh/live/daemon/paths.d.ts +18 -0
- package/dist/lib/mesh/live/daemon/paths.js +31 -0
- package/dist/lib/mesh/live/daemon/pid-lock.d.ts +48 -0
- package/dist/lib/mesh/live/daemon/pid-lock.js +134 -0
- package/dist/lib/mesh/live/daemon/presence.d.ts +101 -0
- package/dist/lib/mesh/live/daemon/presence.js +250 -0
- package/dist/lib/mesh/live/daemon/presign.d.ts +18 -0
- package/dist/lib/mesh/live/daemon/presign.js +65 -0
- package/dist/lib/mesh/live/daemon/run.d.ts +74 -0
- package/dist/lib/mesh/live/daemon/run.js +400 -0
- package/dist/lib/mesh/live/daemon/state.d.ts +24 -0
- package/dist/lib/mesh/live/daemon/state.js +57 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.d.ts +170 -0
- package/dist/lib/mesh/live/daemon/transcript-watch.js +520 -0
- package/dist/lib/mesh/live/enqueue.d.ts +48 -0
- package/dist/lib/mesh/live/enqueue.js +100 -0
- package/dist/lib/mesh/live/flush.d.ts +56 -0
- package/dist/lib/mesh/live/flush.js +360 -0
- package/dist/lib/mesh/live/format-spool-line.d.ts +42 -0
- package/dist/lib/mesh/live/format-spool-line.js +106 -0
- package/dist/lib/mesh/live/index.d.ts +22 -0
- package/dist/lib/mesh/live/index.js +16 -0
- package/dist/lib/mesh/live/paths.d.ts +13 -0
- package/dist/lib/mesh/live/paths.js +30 -0
- package/dist/lib/mesh/live/session-events-client.d.ts +58 -0
- package/dist/lib/mesh/live/session-events-client.js +120 -0
- package/dist/lib/mesh/live/session-identity.d.ts +39 -0
- package/dist/lib/mesh/live/session-identity.js +91 -0
- package/dist/lib/mesh/live/spool.d.ts +52 -0
- package/dist/lib/mesh/live/spool.js +193 -0
- package/dist/lib/mesh/live/ulid.d.ts +12 -0
- package/dist/lib/mesh/live/ulid.js +39 -0
- package/dist/lib/mesh/live/validate-session-event.d.ts +39 -0
- package/dist/lib/mesh/live/validate-session-event.js +128 -0
- package/dist/lib/search-index/index.d.ts +45 -0
- package/dist/lib/search-index/index.js +76 -0
- package/dist/lib/work-context/atomic.d.ts +18 -0
- package/dist/lib/work-context/atomic.js +132 -0
- package/dist/lib/work-context/company.d.ts +103 -0
- package/dist/lib/work-context/company.js +274 -0
- package/dist/lib/work-context/config.d.ts +59 -0
- package/dist/lib/work-context/config.js +172 -0
- package/dist/lib/work-context/contract.d.ts +81 -0
- package/dist/lib/work-context/contract.js +65 -0
- package/dist/lib/work-context/errors.d.ts +27 -0
- package/dist/lib/work-context/errors.js +46 -0
- package/dist/lib/work-context/index.d.ts +23 -0
- package/dist/lib/work-context/index.js +20 -0
- package/dist/lib/work-context/migrate.d.ts +84 -0
- package/dist/lib/work-context/migrate.js +287 -0
- package/dist/lib/work-context/organize.d.ts +130 -0
- package/dist/lib/work-context/organize.js +471 -0
- package/dist/lib/work-context/outbox.d.ts +97 -0
- package/dist/lib/work-context/outbox.js +357 -0
- package/dist/lib/work-context/paths.d.ts +14 -0
- package/dist/lib/work-context/paths.js +39 -0
- package/dist/lib/work-context/project.d.ts +78 -0
- package/dist/lib/work-context/project.js +247 -0
- package/dist/lib/work-context/reconcile.d.ts +85 -0
- package/dist/lib/work-context/reconcile.js +651 -0
- package/dist/lib/work-context/repo-remote.d.ts +43 -0
- package/dist/lib/work-context/repo-remote.js +200 -0
- package/dist/lib/work-context/state.d.ts +112 -0
- package/dist/lib/work-context/state.js +162 -0
- package/dist/main.js +40 -3
- package/dist/utils/cognito-session.d.ts +85 -9
- package/dist/utils/cognito-session.js +216 -9
- package/dist/utils/qmd-model-download-error.d.ts +27 -0
- package/dist/utils/qmd-model-download-error.js +114 -0
- package/dist/utils/qmd-query-document-error.d.ts +23 -0
- package/dist/utils/qmd-query-document-error.js +117 -0
- package/dist/utils/sentry-fingerprint.js +2 -0
- package/package.json +5 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /v1/work-mesh/session-events batch client.
|
|
3
|
+
* Uses Cognito session (ensureCognitoToken / requireToken) and DEFAULT_VAULT_API_URL.
|
|
4
|
+
* Never reads or writes sessions-bind.json.
|
|
5
|
+
*/
|
|
6
|
+
import type { ClassificationState } from "../../work-context/contract.js";
|
|
7
|
+
export declare const SESSION_EVENTS_PATH = "/v1/work-mesh/session-events";
|
|
8
|
+
export declare const SESSION_EVENTS_BATCH_MAX = 100;
|
|
9
|
+
/** Writable contextStatus values accepted by the server (contract §2.2). */
|
|
10
|
+
export declare const WRITABLE_CONTEXT_STATUSES: Set<ClassificationState>;
|
|
11
|
+
export interface SessionEventsBatchBody {
|
|
12
|
+
contractVersion: 1;
|
|
13
|
+
companyUid: string;
|
|
14
|
+
sessionId: string;
|
|
15
|
+
contextStatus: ClassificationState;
|
|
16
|
+
bindingEpisodeId?: string;
|
|
17
|
+
projectId?: string;
|
|
18
|
+
taskId?: string;
|
|
19
|
+
events: Record<string, unknown>[];
|
|
20
|
+
}
|
|
21
|
+
export interface SessionEventsPostResult {
|
|
22
|
+
status: number;
|
|
23
|
+
body: unknown;
|
|
24
|
+
ok: boolean;
|
|
25
|
+
retryable: boolean;
|
|
26
|
+
networkError?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Contract §2.3 batch ingest ack (parsed from a 2xx body). */
|
|
29
|
+
export interface SessionEventsBatchAck {
|
|
30
|
+
accepted: number;
|
|
31
|
+
duplicates: number;
|
|
32
|
+
rejected: Array<{
|
|
33
|
+
eventId: string;
|
|
34
|
+
code: string;
|
|
35
|
+
}>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Parse a batch ingest response body. Returns null when the body is not an
|
|
39
|
+
* object (caller should retain events rather than treating them as posted).
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseSessionEventsBatchAck(body: unknown): SessionEventsBatchAck | null;
|
|
42
|
+
export type SessionEventsPoster = (body: SessionEventsBatchBody) => Promise<SessionEventsPostResult>;
|
|
43
|
+
export declare function classifySessionEventsResponse(status: number, networkError?: boolean): Pick<SessionEventsPostResult, "ok" | "retryable" | "networkError">;
|
|
44
|
+
/**
|
|
45
|
+
* Build a poster that uses vaultApiFetch + Cognito token.
|
|
46
|
+
* `post` override is for tests (never hits the network).
|
|
47
|
+
*/
|
|
48
|
+
export declare function createSessionEventsPoster(opts: {
|
|
49
|
+
token: string;
|
|
50
|
+
baseUrl?: string;
|
|
51
|
+
post?: (path: string, body: SessionEventsBatchBody) => Promise<{
|
|
52
|
+
status: number;
|
|
53
|
+
body: unknown;
|
|
54
|
+
}>;
|
|
55
|
+
}): SessionEventsPoster;
|
|
56
|
+
/** Resolve API base: HQ_VAULT_API_URL overrides DEFAULT_VAULT_API_URL. */
|
|
57
|
+
export declare function resolveVaultApiBase(env?: NodeJS.ProcessEnv): string;
|
|
58
|
+
//# sourceMappingURL=session-events-client.d.ts.map
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /v1/work-mesh/session-events batch client.
|
|
3
|
+
* Uses Cognito session (ensureCognitoToken / requireToken) and DEFAULT_VAULT_API_URL.
|
|
4
|
+
* Never reads or writes sessions-bind.json.
|
|
5
|
+
*/
|
|
6
|
+
import { vaultApiFetch } from "../../../utils/vault-api.js";
|
|
7
|
+
import { DEFAULT_VAULT_API_URL } from "../../../utils/cognito-session.js";
|
|
8
|
+
export const SESSION_EVENTS_PATH = "/v1/work-mesh/session-events";
|
|
9
|
+
export const SESSION_EVENTS_BATCH_MAX = 100;
|
|
10
|
+
/** Writable contextStatus values accepted by the server (contract §2.2). */
|
|
11
|
+
export const WRITABLE_CONTEXT_STATUSES = new Set([
|
|
12
|
+
"unresolved",
|
|
13
|
+
"needs_project",
|
|
14
|
+
"needs_task",
|
|
15
|
+
"bound",
|
|
16
|
+
"migration_pending",
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Parse a batch ingest response body. Returns null when the body is not an
|
|
20
|
+
* object (caller should retain events rather than treating them as posted).
|
|
21
|
+
*/
|
|
22
|
+
export function parseSessionEventsBatchAck(body) {
|
|
23
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
24
|
+
return null;
|
|
25
|
+
const rec = body;
|
|
26
|
+
const accepted = typeof rec.accepted === "number" ? rec.accepted : 0;
|
|
27
|
+
const duplicates = typeof rec.duplicates === "number" ? rec.duplicates : 0;
|
|
28
|
+
const rejected = [];
|
|
29
|
+
if (Array.isArray(rec.rejected)) {
|
|
30
|
+
for (const row of rec.rejected) {
|
|
31
|
+
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
32
|
+
continue;
|
|
33
|
+
const r = row;
|
|
34
|
+
if (typeof r.eventId !== "string" || !r.eventId.trim())
|
|
35
|
+
continue;
|
|
36
|
+
const code = typeof r.code === "string"
|
|
37
|
+
? r.code
|
|
38
|
+
: typeof r.code === "number"
|
|
39
|
+
? String(r.code)
|
|
40
|
+
: "REJECTED";
|
|
41
|
+
rejected.push({ eventId: r.eventId.trim(), code });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { accepted, duplicates, rejected };
|
|
45
|
+
}
|
|
46
|
+
export function classifySessionEventsResponse(status, networkError = false) {
|
|
47
|
+
if (networkError || status === 0) {
|
|
48
|
+
return { ok: false, retryable: true, networkError: true };
|
|
49
|
+
}
|
|
50
|
+
if (status >= 200 && status < 300) {
|
|
51
|
+
return { ok: true, retryable: false };
|
|
52
|
+
}
|
|
53
|
+
if (status === 429 || status >= 500) {
|
|
54
|
+
return { ok: false, retryable: true };
|
|
55
|
+
}
|
|
56
|
+
// Other 4xx → dead-letter (not retryable).
|
|
57
|
+
return { ok: false, retryable: false };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build a poster that uses vaultApiFetch + Cognito token.
|
|
61
|
+
* `post` override is for tests (never hits the network).
|
|
62
|
+
*/
|
|
63
|
+
export function createSessionEventsPoster(opts) {
|
|
64
|
+
return async (body) => {
|
|
65
|
+
try {
|
|
66
|
+
if (opts.post) {
|
|
67
|
+
const res = await opts.post(SESSION_EVENTS_PATH, body);
|
|
68
|
+
return {
|
|
69
|
+
status: res.status,
|
|
70
|
+
body: res.body,
|
|
71
|
+
...classifySessionEventsResponse(res.status),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const res = await vaultApiFetch({
|
|
75
|
+
token: opts.token,
|
|
76
|
+
path: SESSION_EVENTS_PATH,
|
|
77
|
+
method: "POST",
|
|
78
|
+
body: body,
|
|
79
|
+
baseUrl: opts.baseUrl ?? DEFAULT_VAULT_API_URL,
|
|
80
|
+
});
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
let parsed = {};
|
|
83
|
+
if (text) {
|
|
84
|
+
try {
|
|
85
|
+
parsed = JSON.parse(text);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
parsed = { raw: text };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
status: res.status,
|
|
93
|
+
body: parsed,
|
|
94
|
+
...classifySessionEventsResponse(res.status),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
99
|
+
// Auth hard-fail: treat 401/403 text from meshJson-style throws as non-retryable.
|
|
100
|
+
if (/^401\b|^403\b|unauthorized|forbidden/i.test(message)) {
|
|
101
|
+
return {
|
|
102
|
+
status: 401,
|
|
103
|
+
body: { error: message },
|
|
104
|
+
ok: false,
|
|
105
|
+
retryable: false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
status: 0,
|
|
110
|
+
body: { error: message },
|
|
111
|
+
...classifySessionEventsResponse(0, true),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** Resolve API base: HQ_VAULT_API_URL overrides DEFAULT_VAULT_API_URL. */
|
|
117
|
+
export function resolveVaultApiBase(env = process.env) {
|
|
118
|
+
return env.HQ_VAULT_API_URL?.trim() || DEFAULT_VAULT_API_URL;
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=session-events-client.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical session identity for Work Mesh Live (US-007A).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors assets/scaffold/core/scripts/lib/session-id.sh precedence:
|
|
5
|
+
* HQ_SESSION_ID, CLAUDE_CODE_SESSION_ID, CLAUDE_SESSION_ID,
|
|
6
|
+
* CODEX_SESSION_ID, CODEX_THREAD_ID, then the observation's provider session id.
|
|
7
|
+
*
|
|
8
|
+
* Never mints an id. Unstable or missing ids raise InvalidSessionIdentityError.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SESSION_ID_ENV_KEYS: readonly ["HQ_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID"];
|
|
11
|
+
export declare class InvalidSessionIdentityError extends Error {
|
|
12
|
+
readonly code: "InvalidSessionIdentityError";
|
|
13
|
+
constructor(message?: string);
|
|
14
|
+
}
|
|
15
|
+
/** Conservative charset matching session-id.sh (path-safe segment). */
|
|
16
|
+
export declare function isValidSessionId(id: string | undefined | null): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the first valid session id from env (shell precedence), or undefined.
|
|
19
|
+
*/
|
|
20
|
+
export declare function sessionIdFromEnv(env?: NodeJS.ProcessEnv): string | undefined;
|
|
21
|
+
export interface NormalizeSessionIdentityInput {
|
|
22
|
+
/** Observation identity.sessionId (may be empty / placeholder). */
|
|
23
|
+
sessionId?: string;
|
|
24
|
+
/** Observation identity.providerSessionId — last resort after env. */
|
|
25
|
+
providerSessionId?: string;
|
|
26
|
+
env?: NodeJS.ProcessEnv;
|
|
27
|
+
}
|
|
28
|
+
export interface NormalizedSessionIdentity {
|
|
29
|
+
sessionId: string;
|
|
30
|
+
providerSessionId?: string;
|
|
31
|
+
source: "HQ_SESSION_ID" | "CLAUDE_CODE_SESSION_ID" | "CLAUDE_SESSION_ID" | "CODEX_SESSION_ID" | "CODEX_THREAD_ID" | "providerSessionId" | "observation";
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Normalize session identity. Env wins over observation.sessionId;
|
|
35
|
+
* providerSessionId is used only when env yields nothing and observation.sessionId
|
|
36
|
+
* is absent/invalid.
|
|
37
|
+
*/
|
|
38
|
+
export declare function normalizeSessionIdentity(input?: NormalizeSessionIdentityInput): NormalizedSessionIdentity;
|
|
39
|
+
//# sourceMappingURL=session-identity.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical session identity for Work Mesh Live (US-007A).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors assets/scaffold/core/scripts/lib/session-id.sh precedence:
|
|
5
|
+
* HQ_SESSION_ID, CLAUDE_CODE_SESSION_ID, CLAUDE_SESSION_ID,
|
|
6
|
+
* CODEX_SESSION_ID, CODEX_THREAD_ID, then the observation's provider session id.
|
|
7
|
+
*
|
|
8
|
+
* Never mints an id. Unstable or missing ids raise InvalidSessionIdentityError.
|
|
9
|
+
*/
|
|
10
|
+
export const SESSION_ID_ENV_KEYS = [
|
|
11
|
+
"HQ_SESSION_ID",
|
|
12
|
+
"CLAUDE_CODE_SESSION_ID",
|
|
13
|
+
"CLAUDE_SESSION_ID",
|
|
14
|
+
"CODEX_SESSION_ID",
|
|
15
|
+
"CODEX_THREAD_ID",
|
|
16
|
+
];
|
|
17
|
+
export class InvalidSessionIdentityError extends Error {
|
|
18
|
+
code = "InvalidSessionIdentityError";
|
|
19
|
+
constructor(message = "Session identity is missing or invalid") {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "InvalidSessionIdentityError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Conservative charset matching session-id.sh (path-safe segment). */
|
|
25
|
+
export function isValidSessionId(id) {
|
|
26
|
+
if (!id)
|
|
27
|
+
return false;
|
|
28
|
+
const trimmed = id.replace(/\s+/g, "");
|
|
29
|
+
if (!trimmed || trimmed === "." || trimmed === "..")
|
|
30
|
+
return false;
|
|
31
|
+
if (trimmed.length > 128)
|
|
32
|
+
return false;
|
|
33
|
+
return /^[A-Za-z0-9._-]+$/.test(trimmed);
|
|
34
|
+
}
|
|
35
|
+
function sanitize(value) {
|
|
36
|
+
if (value == null)
|
|
37
|
+
return undefined;
|
|
38
|
+
const trimmed = String(value).replace(/[\s]+/g, "");
|
|
39
|
+
return trimmed || undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the first valid session id from env (shell precedence), or undefined.
|
|
43
|
+
*/
|
|
44
|
+
export function sessionIdFromEnv(env = process.env) {
|
|
45
|
+
for (const key of SESSION_ID_ENV_KEYS) {
|
|
46
|
+
const val = sanitize(env[key]);
|
|
47
|
+
if (val && isValidSessionId(val))
|
|
48
|
+
return val;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Normalize session identity. Env wins over observation.sessionId;
|
|
54
|
+
* providerSessionId is used only when env yields nothing and observation.sessionId
|
|
55
|
+
* is absent/invalid.
|
|
56
|
+
*/
|
|
57
|
+
export function normalizeSessionIdentity(input = {}) {
|
|
58
|
+
const env = input.env ?? process.env;
|
|
59
|
+
for (const key of SESSION_ID_ENV_KEYS) {
|
|
60
|
+
const val = sanitize(env[key]);
|
|
61
|
+
if (val && isValidSessionId(val)) {
|
|
62
|
+
const provider = sanitize(input.providerSessionId);
|
|
63
|
+
return {
|
|
64
|
+
sessionId: val,
|
|
65
|
+
providerSessionId: provider && isValidSessionId(provider) ? provider : undefined,
|
|
66
|
+
source: key,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const observed = sanitize(input.sessionId);
|
|
71
|
+
if (observed && isValidSessionId(observed)) {
|
|
72
|
+
const provider = sanitize(input.providerSessionId);
|
|
73
|
+
return {
|
|
74
|
+
sessionId: observed,
|
|
75
|
+
providerSessionId: provider && isValidSessionId(provider) && provider !== observed
|
|
76
|
+
? provider
|
|
77
|
+
: undefined,
|
|
78
|
+
source: "observation",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const provider = sanitize(input.providerSessionId);
|
|
82
|
+
if (provider && isValidSessionId(provider)) {
|
|
83
|
+
return {
|
|
84
|
+
sessionId: provider,
|
|
85
|
+
providerSessionId: provider,
|
|
86
|
+
source: "providerSessionId",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
throw new InvalidSessionIdentityError("No stable session id in env or observation (refusing to mint one)");
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=session-identity.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only spool / held / dead-letter under ~/.hq/work-mesh/.
|
|
3
|
+
* Create 0600; append with O_APPEND so concurrent writers never interleave
|
|
4
|
+
* partial lines (one writeSync per line, under PIPE_BUF).
|
|
5
|
+
*/
|
|
6
|
+
/** Linux PIPE_BUF is typically 4096; keep a conservative ceiling for one line. */
|
|
7
|
+
export declare const SPOOL_LINE_MAX_BYTES = 4096;
|
|
8
|
+
export declare class SpoolWriteError extends Error {
|
|
9
|
+
readonly code: "SPOOL_WRITE_ERROR";
|
|
10
|
+
constructor(message: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Ensure the jsonl file exists at mode 0600 inside an owner-only directory.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ensureSpoolFile(filePath: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Append one JSON line (without newline in `line`) as a single write under O_APPEND.
|
|
18
|
+
*/
|
|
19
|
+
export declare function appendJsonlLine(filePath: string, line: string): void;
|
|
20
|
+
export declare function appendSpoolLine(line: string, root?: string): string;
|
|
21
|
+
export declare function appendHeldLine(line: string, root?: string): string;
|
|
22
|
+
export declare function appendDeadLetterLine(line: string, root?: string): string;
|
|
23
|
+
export declare function appendDroppedReceipt(receipt: Record<string, unknown>, root?: string): string;
|
|
24
|
+
export interface ClaimResult {
|
|
25
|
+
claimPath: string | null;
|
|
26
|
+
lines: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Claim a jsonl file by renaming it aside (never read-then-truncate).
|
|
30
|
+
* Concurrent appends after the rename land on a fresh file.
|
|
31
|
+
*/
|
|
32
|
+
export declare function claimJsonlByRename(sourcePath: string, claimPath: string): ClaimResult;
|
|
33
|
+
export declare function claimSpool(root: string, ts?: string): ClaimResult;
|
|
34
|
+
export declare function claimHeld(root: string, ts?: string): ClaimResult;
|
|
35
|
+
/**
|
|
36
|
+
* List orphan claim files left by a crash mid-flush.
|
|
37
|
+
* Process these before claiming live spool/held (idempotent).
|
|
38
|
+
*/
|
|
39
|
+
export declare function listOrphanClaimFiles(root: string): string[];
|
|
40
|
+
export declare function readClaimFileLines(claimPath: string): string[];
|
|
41
|
+
/**
|
|
42
|
+
* Recover orphaned claim files: read their lines and return paths to unlink
|
|
43
|
+
* after the flush dispositions them.
|
|
44
|
+
*/
|
|
45
|
+
export declare function recoverOrphanClaims(root: string): {
|
|
46
|
+
lines: string[];
|
|
47
|
+
claimPaths: string[];
|
|
48
|
+
};
|
|
49
|
+
export declare function removeClaimFile(claimPath: string | null): void;
|
|
50
|
+
export declare function deadLetterNonEmpty(root?: string): boolean;
|
|
51
|
+
export declare function countJsonlLines(filePath: string): number;
|
|
52
|
+
//# sourceMappingURL=spool.d.ts.map
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only spool / held / dead-letter under ~/.hq/work-mesh/.
|
|
3
|
+
* Create 0600; append with O_APPEND so concurrent writers never interleave
|
|
4
|
+
* partial lines (one writeSync per line, under PIPE_BUF).
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { ensureOwnerDir } from "../../work-context/atomic.js";
|
|
9
|
+
import { workMeshClaimPath, workMeshDeadLetterPath, workMeshDroppedReceiptPath, workMeshHeldPath, workMeshRoot, workMeshSpoolPath, } from "./paths.js";
|
|
10
|
+
/** Linux PIPE_BUF is typically 4096; keep a conservative ceiling for one line. */
|
|
11
|
+
export const SPOOL_LINE_MAX_BYTES = 4096;
|
|
12
|
+
export class SpoolWriteError extends Error {
|
|
13
|
+
code = "SPOOL_WRITE_ERROR";
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "SpoolWriteError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function chmodOwnerFile(filePath) {
|
|
20
|
+
try {
|
|
21
|
+
fs.chmodSync(filePath, 0o600);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// ignore exotic FS
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Ensure the jsonl file exists at mode 0600 inside an owner-only directory.
|
|
29
|
+
*/
|
|
30
|
+
export function ensureSpoolFile(filePath) {
|
|
31
|
+
const dir = ensureOwnerDir(path.dirname(filePath));
|
|
32
|
+
const resolved = path.join(dir, path.basename(filePath));
|
|
33
|
+
if (!fs.existsSync(resolved)) {
|
|
34
|
+
const fd = fs.openSync(resolved, "wx", 0o600);
|
|
35
|
+
fs.closeSync(fd);
|
|
36
|
+
}
|
|
37
|
+
chmodOwnerFile(resolved);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Append one JSON line (without newline in `line`) as a single write under O_APPEND.
|
|
41
|
+
*/
|
|
42
|
+
export function appendJsonlLine(filePath, line) {
|
|
43
|
+
const payload = `${line}\n`;
|
|
44
|
+
const buf = Buffer.from(payload, "utf8");
|
|
45
|
+
if (buf.length > SPOOL_LINE_MAX_BYTES) {
|
|
46
|
+
throw new SpoolWriteError(`Spool line exceeds PIPE_BUF budget (${buf.length} > ${SPOOL_LINE_MAX_BYTES})`);
|
|
47
|
+
}
|
|
48
|
+
ensureSpoolFile(filePath);
|
|
49
|
+
const fd = fs.openSync(filePath, fs.constants.O_APPEND | fs.constants.O_CREAT | fs.constants.O_WRONLY, 0o600);
|
|
50
|
+
try {
|
|
51
|
+
const written = fs.writeSync(fd, buf, 0, buf.length);
|
|
52
|
+
if (written !== buf.length) {
|
|
53
|
+
throw new SpoolWriteError(`Short write to spool (${written} of ${buf.length} bytes)`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
fs.closeSync(fd);
|
|
58
|
+
}
|
|
59
|
+
chmodOwnerFile(filePath);
|
|
60
|
+
}
|
|
61
|
+
export function appendSpoolLine(line, root) {
|
|
62
|
+
const spool = workMeshSpoolPath(root ?? workMeshRoot());
|
|
63
|
+
appendJsonlLine(spool, line);
|
|
64
|
+
return spool;
|
|
65
|
+
}
|
|
66
|
+
export function appendHeldLine(line, root) {
|
|
67
|
+
const held = workMeshHeldPath(root ?? workMeshRoot());
|
|
68
|
+
appendJsonlLine(held, line);
|
|
69
|
+
return held;
|
|
70
|
+
}
|
|
71
|
+
export function appendDeadLetterLine(line, root) {
|
|
72
|
+
const dl = workMeshDeadLetterPath(root ?? workMeshRoot());
|
|
73
|
+
appendJsonlLine(dl, line);
|
|
74
|
+
return dl;
|
|
75
|
+
}
|
|
76
|
+
export function appendDroppedReceipt(receipt, root) {
|
|
77
|
+
const dest = workMeshDroppedReceiptPath(root ?? workMeshRoot());
|
|
78
|
+
appendJsonlLine(dest, JSON.stringify(receipt));
|
|
79
|
+
return dest;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Claim a jsonl file by renaming it aside (never read-then-truncate).
|
|
83
|
+
* Concurrent appends after the rename land on a fresh file.
|
|
84
|
+
*/
|
|
85
|
+
export function claimJsonlByRename(sourcePath, claimPath) {
|
|
86
|
+
try {
|
|
87
|
+
fs.renameSync(sourcePath, claimPath);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
const code = err?.code;
|
|
91
|
+
if (code === "ENOENT") {
|
|
92
|
+
return { claimPath: null, lines: [] };
|
|
93
|
+
}
|
|
94
|
+
throw err;
|
|
95
|
+
}
|
|
96
|
+
const raw = fs.readFileSync(claimPath, "utf8");
|
|
97
|
+
const lines = [];
|
|
98
|
+
for (const line of raw.split("\n")) {
|
|
99
|
+
if (!line.trim())
|
|
100
|
+
continue;
|
|
101
|
+
lines.push(line);
|
|
102
|
+
}
|
|
103
|
+
return { claimPath, lines };
|
|
104
|
+
}
|
|
105
|
+
export function claimSpool(root, ts = `${Date.now()}`) {
|
|
106
|
+
ensureOwnerDir(root);
|
|
107
|
+
return claimJsonlByRename(workMeshSpoolPath(root), workMeshClaimPath("spool", ts, root));
|
|
108
|
+
}
|
|
109
|
+
export function claimHeld(root, ts = `${Date.now()}`) {
|
|
110
|
+
ensureOwnerDir(root);
|
|
111
|
+
return claimJsonlByRename(workMeshHeldPath(root), workMeshClaimPath("held", ts, root));
|
|
112
|
+
}
|
|
113
|
+
/** Match leftover claim files: spool.<ts>.claimed / held.<ts>.claimed (and held.<ts>.held.claimed). */
|
|
114
|
+
const ORPHAN_CLAIM_RE = /^(spool|held)\..+\.claimed$/;
|
|
115
|
+
/**
|
|
116
|
+
* List orphan claim files left by a crash mid-flush.
|
|
117
|
+
* Process these before claiming live spool/held (idempotent).
|
|
118
|
+
*/
|
|
119
|
+
export function listOrphanClaimFiles(root) {
|
|
120
|
+
if (!fs.existsSync(root))
|
|
121
|
+
return [];
|
|
122
|
+
const names = fs.readdirSync(root);
|
|
123
|
+
const out = [];
|
|
124
|
+
for (const name of names) {
|
|
125
|
+
if (!ORPHAN_CLAIM_RE.test(name))
|
|
126
|
+
continue;
|
|
127
|
+
out.push(path.join(root, name));
|
|
128
|
+
}
|
|
129
|
+
return out.sort();
|
|
130
|
+
}
|
|
131
|
+
export function readClaimFileLines(claimPath) {
|
|
132
|
+
if (!fs.existsSync(claimPath))
|
|
133
|
+
return [];
|
|
134
|
+
try {
|
|
135
|
+
const raw = fs.readFileSync(claimPath, "utf8");
|
|
136
|
+
const lines = [];
|
|
137
|
+
for (const line of raw.split("\n")) {
|
|
138
|
+
if (!line.trim())
|
|
139
|
+
continue;
|
|
140
|
+
lines.push(line);
|
|
141
|
+
}
|
|
142
|
+
return lines;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Recover orphaned claim files: read their lines and return paths to unlink
|
|
150
|
+
* after the flush dispositions them.
|
|
151
|
+
*/
|
|
152
|
+
export function recoverOrphanClaims(root) {
|
|
153
|
+
const claimPaths = listOrphanClaimFiles(root);
|
|
154
|
+
const lines = [];
|
|
155
|
+
for (const p of claimPaths) {
|
|
156
|
+
lines.push(...readClaimFileLines(p));
|
|
157
|
+
}
|
|
158
|
+
return { lines, claimPaths };
|
|
159
|
+
}
|
|
160
|
+
export function removeClaimFile(claimPath) {
|
|
161
|
+
if (!claimPath)
|
|
162
|
+
return;
|
|
163
|
+
try {
|
|
164
|
+
fs.unlinkSync(claimPath);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// leave for audit if unlink fails
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export function deadLetterNonEmpty(root) {
|
|
171
|
+
const dl = workMeshDeadLetterPath(root ?? workMeshRoot());
|
|
172
|
+
if (!fs.existsSync(dl))
|
|
173
|
+
return false;
|
|
174
|
+
try {
|
|
175
|
+
const st = fs.statSync(dl);
|
|
176
|
+
return st.size > 0;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export function countJsonlLines(filePath) {
|
|
183
|
+
if (!fs.existsSync(filePath))
|
|
184
|
+
return 0;
|
|
185
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
186
|
+
let n = 0;
|
|
187
|
+
for (const line of raw.split("\n")) {
|
|
188
|
+
if (line.trim())
|
|
189
|
+
n += 1;
|
|
190
|
+
}
|
|
191
|
+
return n;
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=spool.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crockford-base32 ULID (26 chars). Matches the session-event schema pattern
|
|
3
|
+
* and the shell generator in assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh.
|
|
4
|
+
*/
|
|
5
|
+
/** Encode an unsigned integer into `length` Crockford base32 characters (big-endian). */
|
|
6
|
+
export declare function encodeCrockford(value: bigint, length: number): string;
|
|
7
|
+
/**
|
|
8
|
+
* Generate a ULID: 48-bit timestamp (ms) + 80-bit randomness → 26 Crockford chars.
|
|
9
|
+
*/
|
|
10
|
+
export declare function generateUlid(nowMs?: number, entropy?: Buffer): string;
|
|
11
|
+
export declare function isUlid(value: string): boolean;
|
|
12
|
+
//# sourceMappingURL=ulid.d.ts.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crockford-base32 ULID (26 chars). Matches the session-event schema pattern
|
|
3
|
+
* and the shell generator in assets/scaffold/core/scripts/lib/work-mesh-enqueue.sh.
|
|
4
|
+
*/
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
7
|
+
/** Encode an unsigned integer into `length` Crockford base32 characters (big-endian). */
|
|
8
|
+
export function encodeCrockford(value, length) {
|
|
9
|
+
let n = value;
|
|
10
|
+
const chars = new Array(length);
|
|
11
|
+
for (let i = length - 1; i >= 0; i -= 1) {
|
|
12
|
+
chars[i] = CROCKFORD[Number(n & 0x1fn)];
|
|
13
|
+
n >>= 5n;
|
|
14
|
+
}
|
|
15
|
+
return chars.join("");
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Generate a ULID: 48-bit timestamp (ms) + 80-bit randomness → 26 Crockford chars.
|
|
19
|
+
*/
|
|
20
|
+
export function generateUlid(nowMs = Date.now(), entropy) {
|
|
21
|
+
const time = BigInt(nowMs) & 0xffffffffffffn;
|
|
22
|
+
const timePart = encodeCrockford(time, 10);
|
|
23
|
+
const bytes = entropy ?? randomBytes(10);
|
|
24
|
+
if (bytes.length < 10) {
|
|
25
|
+
throw new Error("ULID entropy must be at least 10 bytes");
|
|
26
|
+
}
|
|
27
|
+
let rand = 0n;
|
|
28
|
+
for (let i = 0; i < 10; i += 1) {
|
|
29
|
+
rand = (rand << 8n) | BigInt(bytes[i]);
|
|
30
|
+
}
|
|
31
|
+
// 80 bits → 16 Crockford chars
|
|
32
|
+
const randPart = encodeCrockford(rand, 16);
|
|
33
|
+
return timePart + randPart;
|
|
34
|
+
}
|
|
35
|
+
const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
36
|
+
export function isUlid(value) {
|
|
37
|
+
return ULID_RE.test(value);
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=ulid.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Work Mesh Live — session-event validator (US-001).
|
|
3
|
+
*
|
|
4
|
+
* Uses Ajv draft 2020-12 against the verbatim schema copy. Prohibited content
|
|
5
|
+
* classes are rejected (additionalProperties: false). Error messages never
|
|
6
|
+
* echo prohibited marker values (redaction assertion surface).
|
|
7
|
+
*/
|
|
8
|
+
import type { ValidateFunction } from "ajv";
|
|
9
|
+
export declare const SESSION_EVENT_SCHEMA_PATH: string;
|
|
10
|
+
/** Expected SHA-256 of the canonical schema bytes (hq-pro + hq-cli must match). */
|
|
11
|
+
export declare const SESSION_EVENT_SCHEMA_SHA256 = "be300120b4793a5c6f1e713bf9f72b2fd22f6ac082bef8eedfa507b684b09ccb";
|
|
12
|
+
export declare const PROHIBITED_CONTENT_CLASSES: readonly ["prompts", "model_output", "transcripts", "message_bodies", "tokens", "credentials"];
|
|
13
|
+
export type ProhibitedContentClass = (typeof PROHIBITED_CONTENT_CLASSES)[number];
|
|
14
|
+
/** Fixture field name → prohibited class. */
|
|
15
|
+
export declare const PROHIBITED_FIELD_BY_CLASS: Record<ProhibitedContentClass, string>;
|
|
16
|
+
export interface SessionEventValidationResult {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
errors: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function getSessionEventValidator(): ValidateFunction;
|
|
21
|
+
/**
|
|
22
|
+
* Redact known prohibited markers from Ajv error text so diagnostics never
|
|
23
|
+
* leak prompt/token/credential material.
|
|
24
|
+
*/
|
|
25
|
+
export declare function redactValidationMessage(message: string): string;
|
|
26
|
+
export declare function validateSessionEvent(candidate: unknown): SessionEventValidationResult;
|
|
27
|
+
export declare function sha256File(path: string): string;
|
|
28
|
+
export declare function sha256SessionEventSchema(): string;
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the hq-pro canonical schema path when available.
|
|
31
|
+
*
|
|
32
|
+
* Order: `HQ_PRO_SCHEMA_PATH` env override (absolute path to the hq-pro
|
|
33
|
+
* schema file) → sibling checkout
|
|
34
|
+
* `../hq-pro/src/vault-service/work-mesh/live/session-event.schema.json`
|
|
35
|
+
* from the hq-cli repo root → null.
|
|
36
|
+
* Shipped code must not hardcode machine-specific worktree paths.
|
|
37
|
+
*/
|
|
38
|
+
export declare function resolveHqProSchemaPath(): string | null;
|
|
39
|
+
//# sourceMappingURL=validate-session-event.d.ts.map
|