@basou/sdk 0.4.0 → 0.6.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/dist/index.d.ts +162 -2
- package/dist/index.js +170 -2
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,163 @@
|
|
|
1
|
-
|
|
1
|
+
import { Manifest, StatusSnapshot, SessionEntry, Event, TaskDocument, LoadedApproval, WorkStatsResult } from '@basou/core';
|
|
2
|
+
export { ActiveTimeBasis, Approval, ApprovalStatus, CommandExecutedEvent, DayWorkStats, DecisionRecordedEvent, Event, FileChangedEvent, LoadedApproval, Manifest, MeasureAvailability, NoteAddedEvent, RiskLevel, Session, SessionEndedEvent, SessionEntry, SessionMetrics, SessionSourceKind, SessionStartedEvent, SessionStatus, SessionStatusChangedEvent, SessionWorkStats, SourceWorkStats, StatusCount, StatusSnapshot, SuspectReason, Task, TaskDocument, TaskStatus, TokenTotals, WorkStatsResult, WorkStatsTotals } from '@basou/core';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Base class for every error the SDK throws on its own behalf. Errors that
|
|
6
|
+
* originate in `@basou/core` (e.g. a malformed `session.yaml`) propagate as-is;
|
|
7
|
+
* only the SDK's own preconditions are wrapped, so `instanceof BasouSdkError`
|
|
8
|
+
* identifies "the SDK rejected this call" rather than "the data was bad".
|
|
9
|
+
*/
|
|
10
|
+
declare class BasouSdkError extends Error {
|
|
11
|
+
constructor(message: string, options?: {
|
|
12
|
+
cause?: unknown;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* `openWorkspace` was pointed at a path that is not a usable Basou workspace:
|
|
17
|
+
* the `.basou/` directory is missing, is a symlink, or is otherwise not a
|
|
18
|
+
* directory. The offending repository root is on {@link root}.
|
|
19
|
+
*/
|
|
20
|
+
declare class WorkspaceNotFoundError extends BasouSdkError {
|
|
21
|
+
readonly root: string;
|
|
22
|
+
constructor(root: string, options?: {
|
|
23
|
+
cause?: unknown;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A session / task id prefix matched more than one record. The {@link input}
|
|
28
|
+
* is the prefix as given; the caller should retry with a longer one. (A prefix
|
|
29
|
+
* that matches nothing is NOT an error — the lookup returns `null` instead.)
|
|
30
|
+
*/
|
|
31
|
+
declare class AmbiguousIdError extends BasouSdkError {
|
|
32
|
+
readonly input: string;
|
|
33
|
+
constructor(input: string, options?: {
|
|
34
|
+
cause?: unknown;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A degradation the SDK noticed while reading provenance: a malformed event
|
|
40
|
+
* line, or a session / task that could not be loaded. Best-effort reads skip
|
|
41
|
+
* these and keep going; pass `onDiagnostic` to {@link openWorkspace} to observe
|
|
42
|
+
* them. `message` is a human-readable summary (it folds in the core
|
|
43
|
+
* `ReplayWarning.kind` or skip-reason); structured fields are intentionally not
|
|
44
|
+
* part of this stable shape.
|
|
45
|
+
*/
|
|
46
|
+
type WorkspaceDiagnostic = {
|
|
47
|
+
/** Human-readable summary of the malformed line / skipped record. */
|
|
48
|
+
message: string;
|
|
49
|
+
/** Session or task id the diagnostic relates to, when known. */
|
|
50
|
+
id?: string;
|
|
51
|
+
};
|
|
52
|
+
/** Options for {@link openWorkspace}; all optional. */
|
|
53
|
+
type WorkspaceOptions = {
|
|
54
|
+
/**
|
|
55
|
+
* Clock used for time-sensitive reads (session "suspect" classification,
|
|
56
|
+
* stats span-to-now, status / approval expiry). Injectable for deterministic
|
|
57
|
+
* callers and tests. Defaults to `() => new Date()`, evaluated per call.
|
|
58
|
+
*/
|
|
59
|
+
now?: () => Date;
|
|
60
|
+
/**
|
|
61
|
+
* Observe a malformed event line or a skipped session / task instead of it
|
|
62
|
+
* being silently dropped. Reads are still best-effort: a diagnostic does not
|
|
63
|
+
* fail the call.
|
|
64
|
+
*/
|
|
65
|
+
onDiagnostic?: (diagnostic: WorkspaceDiagnostic) => void;
|
|
66
|
+
};
|
|
67
|
+
/** Options for {@link Workspace.stats}. */
|
|
68
|
+
type StatsOptions = {
|
|
69
|
+
/**
|
|
70
|
+
* IANA timezone used to bucket the per-day breakdown (native logs are UTC).
|
|
71
|
+
* Defaults to the host's local zone.
|
|
72
|
+
*/
|
|
73
|
+
timeZone?: string;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* A read-only handle on one Basou workspace (`<root>/.basou/`). Every method
|
|
77
|
+
* reads provenance from disk; the SDK exposes no writers. Obtain one with
|
|
78
|
+
* {@link openWorkspace}.
|
|
79
|
+
*
|
|
80
|
+
* Session / task lookups (`getSession`, `getTask`, `readEvents`,
|
|
81
|
+
* `streamEvents`) accept a full id or a unique prefix: a prefix matching
|
|
82
|
+
* nothing yields `null` (or an empty stream), a prefix matching more than one
|
|
83
|
+
* record throws {@link AmbiguousIdError}. `getApproval` takes an exact id only.
|
|
84
|
+
*/
|
|
85
|
+
interface Workspace {
|
|
86
|
+
/** Absolute repository root this workspace was opened at. */
|
|
87
|
+
readonly root: string;
|
|
88
|
+
/** Parsed `manifest.yaml`. */
|
|
89
|
+
manifest(): Promise<Manifest>;
|
|
90
|
+
/** A freshly computed workspace status snapshot (directory presence + manifest). */
|
|
91
|
+
status(): Promise<StatusSnapshot>;
|
|
92
|
+
/** Every session, ULID-ascending, each with its `suspect` classification. */
|
|
93
|
+
listSessions(): Promise<SessionEntry[]>;
|
|
94
|
+
/** One session by id / unique prefix, or `null` if no session matches. */
|
|
95
|
+
getSession(idOrPrefix: string): Promise<SessionEntry | null>;
|
|
96
|
+
/** All events of a session, eagerly, ordered as written. Empty if no match. */
|
|
97
|
+
readEvents(idOrPrefix: string): Promise<Event[]>;
|
|
98
|
+
/** All events of a session as a lazy stream (for large logs). */
|
|
99
|
+
streamEvents(idOrPrefix: string): AsyncIterable<Event>;
|
|
100
|
+
/** Every task (active + lazily-indexed), created-at ascending. */
|
|
101
|
+
listTasks(): Promise<TaskDocument[]>;
|
|
102
|
+
/** One task by id / unique prefix (archived included), or `null`. */
|
|
103
|
+
getTask(idOrPrefix: string): Promise<TaskDocument | null>;
|
|
104
|
+
/** Pending + resolved approvals, fully loaded. */
|
|
105
|
+
listApprovals(): Promise<{
|
|
106
|
+
pending: LoadedApproval[];
|
|
107
|
+
resolved: LoadedApproval[];
|
|
108
|
+
}>;
|
|
109
|
+
/** One approval by exact id (resolved checked first), or `null`. */
|
|
110
|
+
getApproval(id: string): Promise<LoadedApproval | null>;
|
|
111
|
+
/** Aggregated work / time / token stats across the workspace's sessions. */
|
|
112
|
+
stats(options?: StatsOptions): Promise<WorkStatsResult>;
|
|
113
|
+
/** The rendered `handoff.md` body (recomputed, without generated markers). */
|
|
114
|
+
renderHandoff(): Promise<string>;
|
|
115
|
+
/** The rendered `decisions.md` body (recomputed, without generated markers). */
|
|
116
|
+
renderDecisions(): Promise<string>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Resolve the Basou workspace root for a working directory by finding the
|
|
120
|
+
* enclosing git repository root (`.basou/` lives at the repo root). A
|
|
121
|
+
* convenience for the common "I'm somewhere in the repo" case; requires git
|
|
122
|
+
* and a repository. Pass the returned path to {@link openWorkspace}. When you
|
|
123
|
+
* already know the root (CI checkout, a copied `.basou/`), skip this and call
|
|
124
|
+
* {@link openWorkspace} directly — it needs no git.
|
|
125
|
+
*/
|
|
126
|
+
declare function resolveWorkspaceRoot(cwd: string): Promise<string>;
|
|
127
|
+
/**
|
|
128
|
+
* Open a read-only handle on the Basou workspace rooted at `repoRoot` (the
|
|
129
|
+
* directory that contains `.basou/`). Validates that `.basou/` exists and is a
|
|
130
|
+
* real directory; throws {@link WorkspaceNotFoundError} otherwise. No git is
|
|
131
|
+
* required — point it at any directory holding a `.basou/`.
|
|
132
|
+
*/
|
|
133
|
+
declare function openWorkspace(repoRoot: string, options?: WorkspaceOptions): Promise<Workspace>;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* `@basou/sdk` — the stable, read-only programmatic API for reading a Basou
|
|
137
|
+
* workspace's provenance (`.basou/`). It is a thin, ergonomic facade over
|
|
138
|
+
* `@basou/core`'s readers: open a workspace once and query sessions, events,
|
|
139
|
+
* tasks, approvals, status, stats, and the rendered handoff / decisions. No
|
|
140
|
+
* writers are exposed — third-party tooling can read provenance without any
|
|
141
|
+
* risk of mutating it.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* ```ts
|
|
145
|
+
* import { openWorkspace, resolveWorkspaceRoot } from "@basou/sdk";
|
|
146
|
+
*
|
|
147
|
+
* const root = await resolveWorkspaceRoot(process.cwd()); // or pass a known root
|
|
148
|
+
* const ws = await openWorkspace(root);
|
|
149
|
+
* for (const { session, suspect } of await ws.listSessions()) {
|
|
150
|
+
* console.log(session.session.id, session.session.status, suspect);
|
|
151
|
+
* }
|
|
152
|
+
* const stats = await ws.stats();
|
|
153
|
+
* console.log(stats.totals.billableActiveTimeMs);
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* SDK API version, tracking the Basou SDK surface (not the npm package
|
|
158
|
+
* version, which moves in lockstep with the monorepo). `0.2.0` is the first
|
|
159
|
+
* release with a runtime read API; `0.1.0` was types-only.
|
|
160
|
+
*/
|
|
161
|
+
declare const BASOU_SDK_VERSION = "0.2.0";
|
|
162
|
+
|
|
163
|
+
export { AmbiguousIdError, BASOU_SDK_VERSION, BasouSdkError, type StatsOptions, type Workspace, type WorkspaceDiagnostic, WorkspaceNotFoundError, type WorkspaceOptions, openWorkspace, resolveWorkspaceRoot };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,174 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var BasouSdkError = class extends Error {
|
|
3
|
+
constructor(message, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = new.target.name;
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var WorkspaceNotFoundError = class extends BasouSdkError {
|
|
9
|
+
root;
|
|
10
|
+
constructor(root, options) {
|
|
11
|
+
super(
|
|
12
|
+
`No Basou workspace at ${root}: expected a '.basou/' directory (run 'basou init' there first).`,
|
|
13
|
+
options
|
|
14
|
+
);
|
|
15
|
+
this.root = root;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var AmbiguousIdError = class extends BasouSdkError {
|
|
19
|
+
input;
|
|
20
|
+
constructor(input, options) {
|
|
21
|
+
super(`Ambiguous id '${input}': matched more than one record; use a longer prefix.`, options);
|
|
22
|
+
this.input = input;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// src/workspace.ts
|
|
27
|
+
import { join, resolve } from "path";
|
|
28
|
+
import {
|
|
29
|
+
assertBasouRootSafe,
|
|
30
|
+
basouPaths,
|
|
31
|
+
buildStatusSnapshot,
|
|
32
|
+
computeWorkStats,
|
|
33
|
+
enumerateApprovals,
|
|
34
|
+
loadApproval,
|
|
35
|
+
loadSessionEntries,
|
|
36
|
+
loadTaskEntries,
|
|
37
|
+
readAllEvents,
|
|
38
|
+
readManifest,
|
|
39
|
+
readTaskFileWithArchiveFallback,
|
|
40
|
+
renderDecisions,
|
|
41
|
+
renderHandoff,
|
|
42
|
+
replayEvents,
|
|
43
|
+
resolveRepositoryRoot,
|
|
44
|
+
resolveSessionId,
|
|
45
|
+
resolveTaskId
|
|
46
|
+
} from "@basou/core";
|
|
47
|
+
function resolveWorkspaceRoot(cwd) {
|
|
48
|
+
return resolveRepositoryRoot(cwd);
|
|
49
|
+
}
|
|
50
|
+
async function openWorkspace(repoRoot, options = {}) {
|
|
51
|
+
const root = resolve(repoRoot);
|
|
52
|
+
const paths = basouPaths(root);
|
|
53
|
+
try {
|
|
54
|
+
await assertBasouRootSafe(paths.root);
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
throw new WorkspaceNotFoundError(root, { cause });
|
|
57
|
+
}
|
|
58
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
59
|
+
const emit = options.onDiagnostic;
|
|
60
|
+
const onWarning = (warning, id) => emit?.({
|
|
61
|
+
message: `event ${warning.kind}${warning.line ? ` (line ${warning.line})` : ""}`,
|
|
62
|
+
...id !== void 0 ? { id } : {}
|
|
63
|
+
});
|
|
64
|
+
const onSkip = (id, reason) => emit?.({ message: `skipped: ${reason}`, id });
|
|
65
|
+
const resolveSession = (input) => resolveOrNull(() => resolveSessionId(paths, input), input);
|
|
66
|
+
const resolveTask = (input) => resolveOrNull(() => resolveTaskId(paths, input, { includeArchived: true }), input);
|
|
67
|
+
return {
|
|
68
|
+
root,
|
|
69
|
+
manifest: () => readManifest(paths),
|
|
70
|
+
status: async () => buildStatusSnapshot({ manifest: await readManifest(paths), paths, now: now() }),
|
|
71
|
+
listSessions: () => loadSessionEntries(paths, {
|
|
72
|
+
now: now(),
|
|
73
|
+
onWarning: (w, sid) => onWarning(w, sid),
|
|
74
|
+
onSkip
|
|
75
|
+
}),
|
|
76
|
+
getSession: async (idOrPrefix) => {
|
|
77
|
+
const id = await resolveSession(idOrPrefix);
|
|
78
|
+
if (id === null) return null;
|
|
79
|
+
const entries = await loadSessionEntries(paths, {
|
|
80
|
+
now: now(),
|
|
81
|
+
onWarning: (w, sid) => onWarning(w, sid),
|
|
82
|
+
onSkip
|
|
83
|
+
});
|
|
84
|
+
return entries.find((e) => e.sessionId === id) ?? null;
|
|
85
|
+
},
|
|
86
|
+
readEvents: async (idOrPrefix) => {
|
|
87
|
+
const id = await resolveSession(idOrPrefix);
|
|
88
|
+
if (id === null) return [];
|
|
89
|
+
return readAllEvents(join(paths.sessions, id), { onWarning: (w) => onWarning(w, id) });
|
|
90
|
+
},
|
|
91
|
+
streamEvents: (idOrPrefix) => {
|
|
92
|
+
async function* iterate() {
|
|
93
|
+
const id = await resolveSession(idOrPrefix);
|
|
94
|
+
if (id === null) return;
|
|
95
|
+
yield* replayEvents(join(paths.sessions, id), { onWarning: (w) => onWarning(w, id) });
|
|
96
|
+
}
|
|
97
|
+
return iterate();
|
|
98
|
+
},
|
|
99
|
+
listTasks: () => loadTaskEntries(paths, { onSkip }),
|
|
100
|
+
getTask: async (idOrPrefix) => {
|
|
101
|
+
const id = await resolveTask(idOrPrefix);
|
|
102
|
+
if (id === null) return null;
|
|
103
|
+
const { doc } = await readTaskFileWithArchiveFallback(paths, id);
|
|
104
|
+
return doc;
|
|
105
|
+
},
|
|
106
|
+
listApprovals: async () => {
|
|
107
|
+
const ids = await enumerateApprovals(paths);
|
|
108
|
+
const resolvedSet = new Set(ids.resolved);
|
|
109
|
+
const pendingIds = ids.pending.filter((id) => !resolvedSet.has(id));
|
|
110
|
+
const load = async (id) => loadApproval(paths, id);
|
|
111
|
+
const [pending, resolved] = await Promise.all([
|
|
112
|
+
Promise.all(pendingIds.map(load)),
|
|
113
|
+
Promise.all(ids.resolved.map(load))
|
|
114
|
+
]);
|
|
115
|
+
return {
|
|
116
|
+
pending: pending.filter((a) => a !== null),
|
|
117
|
+
resolved: resolved.filter((a) => a !== null)
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
getApproval: (id) => loadApproval(paths, id),
|
|
121
|
+
stats: (statsOptions) => computeWorkStats({
|
|
122
|
+
paths,
|
|
123
|
+
now: now(),
|
|
124
|
+
...statsOptions?.timeZone !== void 0 ? { timeZone: statsOptions.timeZone } : {},
|
|
125
|
+
onWarning: (w, sid) => onWarning(w, sid),
|
|
126
|
+
onSessionSkip: onSkip
|
|
127
|
+
}),
|
|
128
|
+
renderHandoff: async () => {
|
|
129
|
+
const result = await renderHandoff({
|
|
130
|
+
paths,
|
|
131
|
+
nowIso: now().toISOString(),
|
|
132
|
+
onWarning: (w, sid) => onWarning(w, sid),
|
|
133
|
+
onSessionSkip: onSkip,
|
|
134
|
+
onTaskSkip: onSkip
|
|
135
|
+
});
|
|
136
|
+
return result.body;
|
|
137
|
+
},
|
|
138
|
+
renderDecisions: async () => {
|
|
139
|
+
const result = await renderDecisions({
|
|
140
|
+
paths,
|
|
141
|
+
nowIso: now().toISOString(),
|
|
142
|
+
onWarning: (w, sid) => onWarning(w, sid),
|
|
143
|
+
onSessionSkip: onSkip
|
|
144
|
+
});
|
|
145
|
+
return result.body;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
async function resolveOrNull(resolver, input) {
|
|
150
|
+
try {
|
|
151
|
+
return await resolver();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
154
|
+
if (/^Ambiguous (session|task) id /.test(message)) {
|
|
155
|
+
throw new AmbiguousIdError(input, { cause: error });
|
|
156
|
+
}
|
|
157
|
+
if (/^(Session|Task) not found: /.test(message) || /^(Session|Task) id is empty$/.test(message)) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
1
164
|
// src/index.ts
|
|
2
|
-
var BASOU_SDK_VERSION = "0.
|
|
165
|
+
var BASOU_SDK_VERSION = "0.2.0";
|
|
3
166
|
export {
|
|
4
|
-
|
|
167
|
+
AmbiguousIdError,
|
|
168
|
+
BASOU_SDK_VERSION,
|
|
169
|
+
BasouSdkError,
|
|
170
|
+
WorkspaceNotFoundError,
|
|
171
|
+
openWorkspace,
|
|
172
|
+
resolveWorkspaceRoot
|
|
5
173
|
};
|
|
6
174
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export const BASOU_SDK_VERSION = \"0.1.0\";\n"],"mappings":";AAAO,IAAM,oBAAoB;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/workspace.ts","../src/index.ts"],"sourcesContent":["/**\n * Base class for every error the SDK throws on its own behalf. Errors that\n * originate in `@basou/core` (e.g. a malformed `session.yaml`) propagate as-is;\n * only the SDK's own preconditions are wrapped, so `instanceof BasouSdkError`\n * identifies \"the SDK rejected this call\" rather than \"the data was bad\".\n */\nexport class BasouSdkError extends Error {\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n this.name = new.target.name;\n }\n}\n\n/**\n * `openWorkspace` was pointed at a path that is not a usable Basou workspace:\n * the `.basou/` directory is missing, is a symlink, or is otherwise not a\n * directory. The offending repository root is on {@link root}.\n */\nexport class WorkspaceNotFoundError extends BasouSdkError {\n readonly root: string;\n constructor(root: string, options?: { cause?: unknown }) {\n super(\n `No Basou workspace at ${root}: expected a '.basou/' directory (run 'basou init' there first).`,\n options,\n );\n this.root = root;\n }\n}\n\n/**\n * A session / task id prefix matched more than one record. The {@link input}\n * is the prefix as given; the caller should retry with a longer one. (A prefix\n * that matches nothing is NOT an error — the lookup returns `null` instead.)\n */\nexport class AmbiguousIdError extends BasouSdkError {\n readonly input: string;\n constructor(input: string, options?: { cause?: unknown }) {\n super(`Ambiguous id '${input}': matched more than one record; use a longer prefix.`, options);\n this.input = input;\n }\n}\n","import { join, resolve } from \"node:path\";\nimport {\n assertBasouRootSafe,\n basouPaths,\n buildStatusSnapshot,\n computeWorkStats,\n type Event,\n enumerateApprovals,\n type LoadedApproval,\n loadApproval,\n loadSessionEntries,\n loadTaskEntries,\n type Manifest,\n readAllEvents,\n readManifest,\n readTaskFileWithArchiveFallback,\n renderDecisions,\n renderHandoff,\n replayEvents,\n resolveRepositoryRoot,\n resolveSessionId,\n resolveTaskId,\n type SessionEntry,\n type StatusSnapshot,\n type TaskDocument,\n type WorkStatsResult,\n} from \"@basou/core\";\nimport { AmbiguousIdError, WorkspaceNotFoundError } from \"./errors.js\";\n\n/**\n * A degradation the SDK noticed while reading provenance: a malformed event\n * line, or a session / task that could not be loaded. Best-effort reads skip\n * these and keep going; pass `onDiagnostic` to {@link openWorkspace} to observe\n * them. `message` is a human-readable summary (it folds in the core\n * `ReplayWarning.kind` or skip-reason); structured fields are intentionally not\n * part of this stable shape.\n */\nexport type WorkspaceDiagnostic = {\n /** Human-readable summary of the malformed line / skipped record. */\n message: string;\n /** Session or task id the diagnostic relates to, when known. */\n id?: string;\n};\n\n/** Options for {@link openWorkspace}; all optional. */\nexport type WorkspaceOptions = {\n /**\n * Clock used for time-sensitive reads (session \"suspect\" classification,\n * stats span-to-now, status / approval expiry). Injectable for deterministic\n * callers and tests. Defaults to `() => new Date()`, evaluated per call.\n */\n now?: () => Date;\n /**\n * Observe a malformed event line or a skipped session / task instead of it\n * being silently dropped. Reads are still best-effort: a diagnostic does not\n * fail the call.\n */\n onDiagnostic?: (diagnostic: WorkspaceDiagnostic) => void;\n};\n\n/** Options for {@link Workspace.stats}. */\nexport type StatsOptions = {\n /**\n * IANA timezone used to bucket the per-day breakdown (native logs are UTC).\n * Defaults to the host's local zone.\n */\n timeZone?: string;\n};\n\n/**\n * A read-only handle on one Basou workspace (`<root>/.basou/`). Every method\n * reads provenance from disk; the SDK exposes no writers. Obtain one with\n * {@link openWorkspace}.\n *\n * Session / task lookups (`getSession`, `getTask`, `readEvents`,\n * `streamEvents`) accept a full id or a unique prefix: a prefix matching\n * nothing yields `null` (or an empty stream), a prefix matching more than one\n * record throws {@link AmbiguousIdError}. `getApproval` takes an exact id only.\n */\nexport interface Workspace {\n /** Absolute repository root this workspace was opened at. */\n readonly root: string;\n\n /** Parsed `manifest.yaml`. */\n manifest(): Promise<Manifest>;\n /** A freshly computed workspace status snapshot (directory presence + manifest). */\n status(): Promise<StatusSnapshot>;\n\n /** Every session, ULID-ascending, each with its `suspect` classification. */\n listSessions(): Promise<SessionEntry[]>;\n /** One session by id / unique prefix, or `null` if no session matches. */\n getSession(idOrPrefix: string): Promise<SessionEntry | null>;\n /** All events of a session, eagerly, ordered as written. Empty if no match. */\n readEvents(idOrPrefix: string): Promise<Event[]>;\n /** All events of a session as a lazy stream (for large logs). */\n streamEvents(idOrPrefix: string): AsyncIterable<Event>;\n\n /** Every task (active + lazily-indexed), created-at ascending. */\n listTasks(): Promise<TaskDocument[]>;\n /** One task by id / unique prefix (archived included), or `null`. */\n getTask(idOrPrefix: string): Promise<TaskDocument | null>;\n\n /** Pending + resolved approvals, fully loaded. */\n listApprovals(): Promise<{ pending: LoadedApproval[]; resolved: LoadedApproval[] }>;\n /** One approval by exact id (resolved checked first), or `null`. */\n getApproval(id: string): Promise<LoadedApproval | null>;\n\n /** Aggregated work / time / token stats across the workspace's sessions. */\n stats(options?: StatsOptions): Promise<WorkStatsResult>;\n\n /** The rendered `handoff.md` body (recomputed, without generated markers). */\n renderHandoff(): Promise<string>;\n /** The rendered `decisions.md` body (recomputed, without generated markers). */\n renderDecisions(): Promise<string>;\n}\n\n/**\n * Resolve the Basou workspace root for a working directory by finding the\n * enclosing git repository root (`.basou/` lives at the repo root). A\n * convenience for the common \"I'm somewhere in the repo\" case; requires git\n * and a repository. Pass the returned path to {@link openWorkspace}. When you\n * already know the root (CI checkout, a copied `.basou/`), skip this and call\n * {@link openWorkspace} directly — it needs no git.\n */\nexport function resolveWorkspaceRoot(cwd: string): Promise<string> {\n return resolveRepositoryRoot(cwd);\n}\n\n/**\n * Open a read-only handle on the Basou workspace rooted at `repoRoot` (the\n * directory that contains `.basou/`). Validates that `.basou/` exists and is a\n * real directory; throws {@link WorkspaceNotFoundError} otherwise. No git is\n * required — point it at any directory holding a `.basou/`.\n */\nexport async function openWorkspace(\n repoRoot: string,\n options: WorkspaceOptions = {},\n): Promise<Workspace> {\n // Normalize to an absolute path up front so `root` honors its documented\n // absolute-path contract even when the caller passes a relative directory.\n const root = resolve(repoRoot);\n const paths = basouPaths(root);\n try {\n await assertBasouRootSafe(paths.root);\n } catch (cause) {\n throw new WorkspaceNotFoundError(root, { cause });\n }\n const now = options.now ?? (() => new Date());\n const emit = options.onDiagnostic;\n const onWarning = (warning: { kind: string; line?: number }, id?: string): void =>\n emit?.({\n message: `event ${warning.kind}${warning.line ? ` (line ${warning.line})` : \"\"}`,\n ...(id !== undefined ? { id } : {}),\n });\n const onSkip = (id: string, reason: string): void =>\n emit?.({ message: `skipped: ${reason}`, id });\n\n /** Resolve a session prefix to a full id, or null when nothing matches. */\n const resolveSession = (input: string): Promise<string | null> =>\n resolveOrNull(() => resolveSessionId(paths, input), input);\n const resolveTask = (input: string): Promise<string | null> =>\n resolveOrNull(() => resolveTaskId(paths, input, { includeArchived: true }), input);\n\n return {\n root,\n\n manifest: () => readManifest(paths),\n\n status: async () =>\n buildStatusSnapshot({ manifest: await readManifest(paths), paths, now: now() }),\n\n listSessions: () =>\n loadSessionEntries(paths, {\n now: now(),\n onWarning: (w, sid) => onWarning(w, sid),\n onSkip,\n }),\n\n getSession: async (idOrPrefix) => {\n const id = await resolveSession(idOrPrefix);\n if (id === null) return null;\n const entries = await loadSessionEntries(paths, {\n now: now(),\n onWarning: (w, sid) => onWarning(w, sid),\n onSkip,\n });\n return entries.find((e) => e.sessionId === id) ?? null;\n },\n\n readEvents: async (idOrPrefix) => {\n const id = await resolveSession(idOrPrefix);\n if (id === null) return [];\n return readAllEvents(join(paths.sessions, id), { onWarning: (w) => onWarning(w, id) });\n },\n\n streamEvents: (idOrPrefix): AsyncIterable<Event> => {\n async function* iterate(): AsyncGenerator<Event> {\n const id = await resolveSession(idOrPrefix);\n if (id === null) return;\n yield* replayEvents(join(paths.sessions, id), { onWarning: (w) => onWarning(w, id) });\n }\n return iterate();\n },\n\n listTasks: () => loadTaskEntries(paths, { onSkip }),\n\n getTask: async (idOrPrefix) => {\n const id = await resolveTask(idOrPrefix);\n if (id === null) return null;\n const { doc } = await readTaskFileWithArchiveFallback(paths, id);\n return doc;\n },\n\n listApprovals: async () => {\n const ids = await enumerateApprovals(paths);\n // `loadApproval` searches resolved/ before pending/, so an id present in\n // BOTH (a stale pending file left after resolution) would otherwise load\n // the resolved record into the pending list too. Drop those from pending\n // so a resolved approval is reported once, under `resolved`.\n const resolvedSet = new Set(ids.resolved);\n const pendingIds = ids.pending.filter((id) => !resolvedSet.has(id));\n const load = async (id: string): Promise<LoadedApproval | null> => loadApproval(paths, id);\n const [pending, resolved] = await Promise.all([\n Promise.all(pendingIds.map(load)),\n Promise.all(ids.resolved.map(load)),\n ]);\n return {\n pending: pending.filter((a): a is LoadedApproval => a !== null),\n resolved: resolved.filter((a): a is LoadedApproval => a !== null),\n };\n },\n\n getApproval: (id) => loadApproval(paths, id),\n\n stats: (statsOptions) =>\n computeWorkStats({\n paths,\n now: now(),\n ...(statsOptions?.timeZone !== undefined ? { timeZone: statsOptions.timeZone } : {}),\n onWarning: (w, sid) => onWarning(w, sid),\n onSessionSkip: onSkip,\n }),\n\n renderHandoff: async () => {\n const result = await renderHandoff({\n paths,\n nowIso: now().toISOString(),\n onWarning: (w, sid) => onWarning(w, sid),\n onSessionSkip: onSkip,\n onTaskSkip: onSkip,\n });\n return result.body;\n },\n\n renderDecisions: async () => {\n const result = await renderDecisions({\n paths,\n nowIso: now().toISOString(),\n onWarning: (w, sid) => onWarning(w, sid),\n onSessionSkip: onSkip,\n });\n return result.body;\n },\n };\n}\n\n/**\n * Run a core id-resolver and normalize its outcome: a successful resolution\n * returns the id; the \"not found\" / \"empty input\" contract errors map to\n * `null` (no such record); the \"ambiguous\" contract error maps to\n * {@link AmbiguousIdError}. Any other error propagates unchanged.\n */\nasync function resolveOrNull(\n resolver: () => Promise<string>,\n input: string,\n): Promise<string | null> {\n try {\n return await resolver();\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n // Match the core resolver's exact contract strings (id-resolver.ts), not a\n // loose substring, so an unrelated error that merely contains \"not found\"\n // is never silently swallowed to null.\n if (/^Ambiguous (session|task) id /.test(message)) {\n throw new AmbiguousIdError(input, { cause: error });\n }\n if (\n /^(Session|Task) not found: /.test(message) ||\n /^(Session|Task) id is empty$/.test(message)\n ) {\n return null;\n }\n throw error;\n }\n}\n","/**\n * `@basou/sdk` — the stable, read-only programmatic API for reading a Basou\n * workspace's provenance (`.basou/`). It is a thin, ergonomic facade over\n * `@basou/core`'s readers: open a workspace once and query sessions, events,\n * tasks, approvals, status, stats, and the rendered handoff / decisions. No\n * writers are exposed — third-party tooling can read provenance without any\n * risk of mutating it.\n *\n * @example\n * ```ts\n * import { openWorkspace, resolveWorkspaceRoot } from \"@basou/sdk\";\n *\n * const root = await resolveWorkspaceRoot(process.cwd()); // or pass a known root\n * const ws = await openWorkspace(root);\n * for (const { session, suspect } of await ws.listSessions()) {\n * console.log(session.session.id, session.session.status, suspect);\n * }\n * const stats = await ws.stats();\n * console.log(stats.totals.billableActiveTimeMs);\n * ```\n */\n\n/**\n * SDK API version, tracking the Basou SDK surface (not the npm package\n * version, which moves in lockstep with the monorepo). `0.2.0` is the first\n * release with a runtime read API; `0.1.0` was types-only.\n */\nexport const BASOU_SDK_VERSION = \"0.2.0\";\n\n// Read types re-exported from @basou/core so consumers can type the values the\n// SDK returns without depending on @basou/core directly. These track the\n// on-disk provenance schema.\nexport type {\n ActiveTimeBasis,\n Approval,\n ApprovalStatus,\n CommandExecutedEvent,\n DayWorkStats,\n DecisionRecordedEvent,\n Event,\n FileChangedEvent,\n LoadedApproval,\n Manifest,\n MeasureAvailability,\n NoteAddedEvent,\n RiskLevel,\n Session,\n SessionEndedEvent,\n SessionEntry,\n SessionMetrics,\n SessionSourceKind,\n SessionStartedEvent,\n SessionStatus,\n SessionStatusChangedEvent,\n SessionWorkStats,\n SourceWorkStats,\n StatusCount,\n StatusSnapshot,\n SuspectReason,\n Task,\n TaskDocument,\n TaskStatus,\n TokenTotals,\n WorkStatsResult,\n WorkStatsTotals,\n} from \"@basou/core\";\nexport { AmbiguousIdError, BasouSdkError, WorkspaceNotFoundError } from \"./errors.js\";\nexport {\n openWorkspace,\n resolveWorkspaceRoot,\n type StatsOptions,\n type Workspace,\n type WorkspaceDiagnostic,\n type WorkspaceOptions,\n} from \"./workspace.js\";\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basou/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"description": "
|
|
6
|
+
"description": "Read-only SDK for Basou: a stable, ergonomic API to read a workspace's provenance (sessions, events, tasks, approvals, stats).",
|
|
7
7
|
"author": "Takashi Matsuyama",
|
|
8
8
|
"keywords": [
|
|
9
9
|
"ai",
|
|
10
10
|
"agent",
|
|
11
11
|
"sdk",
|
|
12
|
-
"
|
|
12
|
+
"provenance",
|
|
13
13
|
"basou"
|
|
14
14
|
],
|
|
15
15
|
"repository": {
|
|
@@ -41,6 +41,9 @@
|
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=20.10.0"
|
|
43
43
|
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@basou/core": "0.6.0"
|
|
46
|
+
},
|
|
44
47
|
"scripts": {
|
|
45
48
|
"build": "tsup",
|
|
46
49
|
"dev": "tsup --watch",
|