@tekmidian/pai 0.14.1 → 0.15.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.
@@ -10,7 +10,7 @@ import { t as createStorageBackend } from "./factory-Q88X1bAN.mjs";
10
10
  import { t as PaiClient } from "./ipc-client-CoyUHPod.mjs";
11
11
  import { a as expandHome, c as UNROUTED, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig, s as OWNER_LABEL_PREFIX, t as CONFIG_DIR } from "./config-C8m-tPhP.mjs";
12
12
  import { s as kgQuery } from "./kg-entity-LXblD7LZ.mjs";
13
- import { a as renderDedupedSessions, c as revealItermSession, d as fmtAge, f as resolveSessionByNameOrId, i as normalizeName, l as sendToSession, o as callAiBroker, p as scanSessions, r as buildDeduped, s as fetchLiveSessions, u as printExitDir } from "./main-resolver-uFxNiDy7.mjs";
13
+ import { a as renderDedupedSessions, c as revealItermSession, d as fmtAge, f as resolveSessionByNameOrId, i as normalizeName$1, l as sendToSession, o as callAiBroker, p as scanSessions, r as buildDeduped, s as fetchLiveSessions, u as printExitDir } from "./main-resolver-uFxNiDy7.mjs";
14
14
  import { appendFileSync, chmodSync, copyFileSync, createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
15
15
  import { homedir, platform } from "node:os";
16
16
  import { basename, dirname, join, relative, resolve } from "node:path";
@@ -4219,6 +4219,10 @@ async function listProjects(token) {
4219
4219
  name: p.name
4220
4220
  }));
4221
4221
  }
4222
+ /** Strip emoji and case so "Whazaa 🐝" and "whazaa" compare equal. */
4223
+ function normalizeName(raw) {
4224
+ return raw.replace(/[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}\u{FE0F}\u{200D}]/gu, "").trim().toLowerCase();
4225
+ }
4222
4226
  /** Todoist priority is inverted: 4 is urgent, 1 is lowest. */
4223
4227
  function toPriority(wire) {
4224
4228
  switch (wire) {
@@ -4301,13 +4305,14 @@ var TodoistProvider = class {
4301
4305
  if (!token || !this.config.rootProjectId) throw new Error("Todoist provider is not configured — run `pai setup`.");
4302
4306
  const labels = [...task.labels ?? []];
4303
4307
  if (task.owner && !labels.some((l) => l.toLowerCase() === `pai:${task.owner.toLowerCase()}`)) labels.push(`pai:${task.owner}`);
4304
- const sectionId = task.owner ? void 0 : this.config.findingsSectionId;
4308
+ const sectionId = task.owner || task.into ? void 0 : this.config.findingsSectionId;
4309
+ const projectId = task.into ? (await this.findOrCreateSubProject(task.into)).id : this.config.rootProjectId;
4305
4310
  const created = await call(token, "/tasks", {
4306
4311
  method: "POST",
4307
4312
  body: {
4308
4313
  content: task.title,
4309
4314
  description: task.body,
4310
- project_id: this.config.rootProjectId,
4315
+ project_id: projectId,
4311
4316
  ...sectionId ? { section_id: sectionId } : {},
4312
4317
  labels,
4313
4318
  priority: fromPriority(task.priority),
@@ -4343,6 +4348,42 @@ var TodoistProvider = class {
4343
4348
  body: { labels }
4344
4349
  });
4345
4350
  }
4351
+ /**
4352
+ * Find the sub-project named `name` under the bus root, creating it if absent.
4353
+ *
4354
+ * Per-project sub-projects are the filing convention: a flat pile in the root
4355
+ * buries findings across projects. This exists so the convention can be
4356
+ * executed rather than re-derived — a session that has to infer structure
4357
+ * from the existing project list will sometimes infer cautiously and file
4358
+ * flat, which is the mess the convention prevents.
4359
+ *
4360
+ * Matching is case-insensitive and ignores decoration, so "Whazaa" finds an
4361
+ * existing "Whazaa 🐝" rather than creating a near-duplicate.
4362
+ */
4363
+ async findOrCreateSubProject(name) {
4364
+ const token = this.token();
4365
+ if (!token || !this.config.rootProjectId) throw new Error("Todoist provider is not configured — run `pai task config`.");
4366
+ const root = this.config.rootProjectId;
4367
+ const projects = await collect(token, "/projects");
4368
+ const want = normalizeName(name);
4369
+ for (const p of projects) {
4370
+ if (p.is_archived || p.is_deleted) continue;
4371
+ if (p.parent_id === root && normalizeName(p.name) === want) return {
4372
+ id: p.id,
4373
+ created: false
4374
+ };
4375
+ }
4376
+ return {
4377
+ id: (await call(token, "/projects", {
4378
+ method: "POST",
4379
+ body: {
4380
+ name,
4381
+ parent_id: root
4382
+ }
4383
+ })).id,
4384
+ created: true
4385
+ };
4386
+ }
4346
4387
  /** Append a comment — used to keep run history on the task itself. */
4347
4388
  async comment(id, content) {
4348
4389
  const token = this.token();
@@ -7008,8 +7049,11 @@ const HISTORY_LIMIT = 5;
7008
7049
  const EMPTY = {
7009
7050
  ...EMPTY_RUN_STATE,
7010
7051
  history: {},
7011
- lastReported: {}
7052
+ lastReported: {},
7053
+ failedDispatches: {}
7012
7054
  };
7055
+ /** Consecutive failed dispatches before a task is reported as needing attention. */
7056
+ const DISPATCH_FAILURES_BEFORE_ALARM = 3;
7013
7057
  /**
7014
7058
  * Run state is a rebuildable cache, so a damaged file must not block the
7015
7059
  * scheduler forever — starting fresh is the correct recovery here, which is
@@ -7061,10 +7105,15 @@ async function tick(opts) {
7061
7105
  case "running":
7062
7106
  note = `${d.elapsedMinutes}m elapsed`;
7063
7107
  break;
7064
- case "dispatch":
7065
- note = await handleDispatch(task, d.overdueMinutes, opts, state, now);
7066
- if (!opts.dryRun) report.dispatched++;
7108
+ case "dispatch": {
7109
+ const r = await handleDispatch(task, d.overdueMinutes, opts, state, now);
7110
+ note = r.note;
7111
+ if (!opts.dryRun) {
7112
+ report.dispatched++;
7113
+ if (r.alarm) report.stuck++;
7114
+ }
7067
7115
  break;
7116
+ }
7068
7117
  case "complete":
7069
7118
  note = await handleComplete(task, d.durationMinutes, opts, state);
7070
7119
  if (!opts.dryRun) report.completed++;
@@ -7090,8 +7139,14 @@ async function tick(opts) {
7090
7139
  }
7091
7140
  async function handleDispatch(task, overdue, opts, state, now) {
7092
7141
  const late = overdue > 5 ? ` (${overdue}m late)` : "";
7093
- if (opts.dryRun) return `would dispatch to ${task.owner.project ?? "nobody"}${late}`;
7094
- if (!task.owner.project) return "unrouted — cannot dispatch";
7142
+ if (opts.dryRun) return {
7143
+ note: `would dispatch to ${task.owner.project ?? "nobody"}${late}`,
7144
+ alarm: false
7145
+ };
7146
+ if (!task.owner.project) return {
7147
+ note: "unrouted — cannot dispatch",
7148
+ alarm: false
7149
+ };
7095
7150
  const result = await dispatchTask(task, {
7096
7151
  transport: opts.transport,
7097
7152
  autoDispatch: opts.autoDispatch,
@@ -7101,9 +7156,23 @@ async function handleDispatch(task, overdue, opts, state, now) {
7101
7156
  await opts.provider.setLabels(task.id, [...task.labels, RUNNING_LABEL]);
7102
7157
  state.startedAt[task.id] = now;
7103
7158
  delete state.failedProbes[task.id];
7104
- return `${result.outcome} to ${result.session}${late}`;
7159
+ delete state.failedDispatches[task.id];
7160
+ return {
7161
+ note: `${result.outcome} to ${result.session}${late}`,
7162
+ alarm: false
7163
+ };
7105
7164
  }
7106
- return `not dispatched: ${result.outcome}${result.reason ? " — " + result.reason : ""}`;
7165
+ const fails = (state.failedDispatches[task.id] ?? 0) + 1;
7166
+ state.failedDispatches[task.id] = fails;
7167
+ const detail = `${result.outcome}${result.reason ? " — " + result.reason : ""}`;
7168
+ if (fails >= DISPATCH_FAILURES_BEFORE_ALARM) return {
7169
+ note: `NOT RUNNING — ${fails} failed dispatches: ${detail}`,
7170
+ alarm: true
7171
+ };
7172
+ return {
7173
+ note: `not dispatched (${fails}/${DISPATCH_FAILURES_BEFORE_ALARM}): ${detail}`,
7174
+ alarm: false
7175
+ };
7107
7176
  }
7108
7177
  async function handleComplete(task, durationMinutes, opts, state) {
7109
7178
  if (opts.dryRun) return `would clear ${RUNNING_LABEL}, ${durationMinutes ?? "?"}m`;
@@ -7359,7 +7428,7 @@ function registerTaskCommands(taskCmd) {
7359
7428
  limit: opts.limit
7360
7429
  }));
7361
7430
  });
7362
- taskCmd.command("add <title>").description("File a task onto the bus").option("--owner <project>", "PAI project that owns this (adds a pai: label)").option("--body <text>", "Full procedure and reasoning — not just a restatement of the title").option("--due <date>", "Due date (ISO or natural language)").option("--priority <p>", "p1 (highest) … p4 (default)").option("--url <url>", "Reference — prefer a hook:// URL over a file path").action(async (title, opts) => {
7431
+ taskCmd.command("add <title>").description("File a task onto the bus").option("--owner <project>", "PAI project that owns this (adds a pai: label)").option("--body <text>", "Full procedure and reasoning — not just a restatement of the title").option("--due <date>", "Due date (ISO or natural language)").option("--priority <p>", "p1 (highest) … p4 (default)").option("--url <url>", "Reference — prefer a hook:// URL over a file path").option("--into <sub-project>", "File into this sub-project under the bus root, creating it if absent").action(async (title, opts) => {
7363
7432
  const provider = buildProvider();
7364
7433
  if (!provider) return reportUnconfigured();
7365
7434
  if (!opts.body) {
@@ -7372,7 +7441,8 @@ function registerTaskCommands(taskCmd) {
7372
7441
  owner: opts.owner ?? null,
7373
7442
  due: opts.due,
7374
7443
  priority: opts.priority,
7375
- sourceUrl: opts.url
7444
+ sourceUrl: opts.url,
7445
+ into: opts.into
7376
7446
  });
7377
7447
  console.log(chalk.green(` Filed: ${created.title}`));
7378
7448
  console.log(` ${renderOwner(created)} ${dim("· " + created.id)}`);
@@ -9276,7 +9346,7 @@ function buildFeedFrom(allSessions, projects, liveSessions, allProjects) {
9276
9346
  const nameRoot = /* @__PURE__ */ new Map();
9277
9347
  for (const p of allProjects) {
9278
9348
  slugRoot.set(p.slug, p.root_path);
9279
- nameRoot.set(normalizeName(p.display_name ?? p.slug).toLowerCase(), p.root_path);
9349
+ nameRoot.set(normalizeName$1(p.display_name ?? p.slug).toLowerCase(), p.root_path);
9280
9350
  }
9281
9351
  const bestResumable = /* @__PURE__ */ new Map();
9282
9352
  for (const s of allSessions) {
@@ -9679,4 +9749,4 @@ async function cmdPick(db, opts = {}) {
9679
9749
 
9680
9750
  //#endregion
9681
9751
  export { registerDaemonCommands as C, registerProjectsCommands as D, registerRegistryCommands as E, findMovedPath as O, registerBackupCommands as S, registerMemoryCommands as T, registerObservationCommands as _, cmdPauseAll as a, registerSetupCommand as b, cmdPause as c, registerKgCommands as d, registerTopicCommands as f, registerSkillCommands as g, registerUpdateCommand as h, cmdClearNames as i, resolveIdentifier as k, registerHelpCommand as l, registerNotifyCommands as m, cmdFind as n, cmdGoto as o, registerTaskCommands as p, cmdList as r, cmdEnd as s, cmdPick as t, registerDbCommands as u, registerZettelCommands as v, registerMcpCommands as w, registerRestoreCommands as x, registerObsidianCommands as y };
9682
- //# sourceMappingURL=pick-BPlB38eB.mjs.map
9752
+ //# sourceMappingURL=pick-r95zvydr.mjs.map