@ai-ecoverse/slicc-agent 3.48.1 → 3.50.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.
Files changed (89) hide show
  1. package/README.md +38 -6
  2. package/bin/agent +1 -0
  3. package/dist/control.js +1 -1
  4. package/dist/control.js.map +2 -2
  5. package/dist/freezer/index.js +29 -8
  6. package/dist/freezer/index.js.map +2 -2
  7. package/dist/freezer/title.js +2 -2
  8. package/dist/freezer/title.js.map +2 -2
  9. package/dist/gelatiere/catalog.js +1 -1
  10. package/dist/gelatiere/catalog.js.map +2 -2
  11. package/dist/gelatiere/index.js +44 -21
  12. package/dist/gelatiere/index.js.map +2 -2
  13. package/dist/gelatiere/store.js +9 -5
  14. package/dist/gelatiere/store.js.map +2 -2
  15. package/dist/index.js.map +2 -2
  16. package/dist/kernel/accounts.js +141 -0
  17. package/dist/kernel/accounts.js.map +7 -0
  18. package/dist/kernel/agent-users.js +180 -0
  19. package/dist/kernel/agent-users.js.map +7 -0
  20. package/dist/kernel/env.js +9 -3
  21. package/dist/kernel/env.js.map +2 -2
  22. package/dist/kernel/users.js +16 -1
  23. package/dist/kernel/users.js.map +2 -2
  24. package/dist/layout.js +62 -0
  25. package/dist/layout.js.map +7 -0
  26. package/dist/licks/index.js.map +2 -2
  27. package/dist/licks/sources.js +69 -20
  28. package/dist/licks/sources.js.map +2 -2
  29. package/dist/main.js +70 -36
  30. package/dist/main.js.map +2 -2
  31. package/dist/mcp/index.js +5 -3
  32. package/dist/mcp/index.js.map +2 -2
  33. package/dist/mcp/paths.js +15 -0
  34. package/dist/mcp/paths.js.map +7 -0
  35. package/dist/mcp/runtime.js +16 -14
  36. package/dist/mcp/runtime.js.map +3 -3
  37. package/dist/memory/extract.js +2 -1
  38. package/dist/memory/extract.js.map +2 -2
  39. package/dist/memory/prompt.js +5 -1
  40. package/dist/memory/prompt.js.map +2 -2
  41. package/dist/memory/runtime.js +9 -1
  42. package/dist/memory/runtime.js.map +2 -2
  43. package/dist/roles/roles.js +54 -15
  44. package/dist/roles/roles.js.map +2 -2
  45. package/dist/scoops/cli.js +40 -19
  46. package/dist/scoops/cli.js.map +2 -2
  47. package/dist/scoops/extension.js +19 -15
  48. package/dist/scoops/extension.js.map +2 -2
  49. package/dist/scoops/index.js +82 -11
  50. package/dist/scoops/index.js.map +2 -2
  51. package/dist/scoops/requests.js +2 -1
  52. package/dist/scoops/requests.js.map +2 -2
  53. package/dist/scoops/service.js +45 -38
  54. package/dist/scoops/service.js.map +2 -2
  55. package/dist/scoops/sync.js +63 -13
  56. package/dist/scoops/sync.js.map +2 -2
  57. package/dist/skills/index.js +76 -34
  58. package/dist/skills/index.js.map +3 -3
  59. package/dist/sprinkles/index.js +114 -25
  60. package/dist/sprinkles/index.js.map +2 -2
  61. package/dist/sprinkles/sprinkles.js +13 -4
  62. package/dist/sprinkles/sprinkles.js.map +2 -2
  63. package/dist/types/freezer/index.d.ts +3 -1
  64. package/dist/types/freezer/title.d.ts +1 -1
  65. package/dist/types/gelatiere/index.d.ts +8 -0
  66. package/dist/types/gelatiere/store.d.ts +3 -2
  67. package/dist/types/index.d.ts +1 -1
  68. package/dist/types/kernel/accounts.d.ts +29 -0
  69. package/dist/types/kernel/agent-users.d.ts +37 -0
  70. package/dist/types/kernel/env.d.ts +6 -0
  71. package/dist/types/kernel/users.d.ts +15 -0
  72. package/dist/types/layout.d.ts +26 -0
  73. package/dist/types/licks/index.d.ts +2 -1
  74. package/dist/types/licks/sources.d.ts +11 -1
  75. package/dist/types/mcp/paths.d.ts +3 -0
  76. package/dist/types/mcp/runtime.d.ts +3 -3
  77. package/dist/types/memory/prompt.d.ts +1 -0
  78. package/dist/types/memory/runtime.d.ts +1 -0
  79. package/dist/types/roles/roles.d.ts +14 -1
  80. package/dist/types/scoops/cli.d.ts +5 -0
  81. package/dist/types/scoops/extension.d.ts +6 -3
  82. package/dist/types/scoops/index.d.ts +9 -0
  83. package/dist/types/scoops/requests.d.ts +2 -0
  84. package/dist/types/scoops/service.d.ts +5 -3
  85. package/dist/types/scoops/sync.d.ts +9 -6
  86. package/dist/types/skills/index.d.ts +10 -2
  87. package/dist/types/sprinkles/index.d.ts +15 -0
  88. package/dist/types/sprinkles/sprinkles.d.ts +7 -1
  89. package/package.json +4 -4
package/dist/layout.js ADDED
@@ -0,0 +1,62 @@
1
+ import { CONE_PREFIX, homeOf, SCOOP_USER, scoopUser } from "./kernel/accounts.js";
2
+ import { quote } from "./kernel/env.js";
3
+ const SCOOPS_ROOT = "/scoops";
4
+ const SESSIONS = { tmp: "/tmp/agent-sessions", durable: "/home/sessions" };
5
+ const ROOT_USER = "root";
6
+ const AGENT_DIR = "/etc/slicc/agent";
7
+ const ROOT_HOME = "/root";
8
+ const SHARED_DIR = "/var/lib/slicc/agent/shared";
9
+ const LEGACY = {
10
+ users: false,
11
+ scoopRoot: (folder) => `${SCOOPS_ROOT}/${folder}`,
12
+ workspace: (folder) => `${SCOOPS_ROOT}/${folder}/workspace`,
13
+ reports: (folder) => `${SCOOPS_ROOT}/${folder}/reports`,
14
+ sessions: (persist) => SESSIONS[persist],
15
+ homeOf: () => "/home",
16
+ envFor: async () => void 0,
17
+ ownDir: async () => void 0
18
+ };
19
+ function agentOfUser(state, user) {
20
+ if (user.startsWith(SCOOP_USER)) {
21
+ const folder = user.slice(SCOOP_USER.length);
22
+ return Object.entries(state.scoops).find(([, record]) => record.folder === folder)?.[0];
23
+ }
24
+ return state.cones[user] || Object.values(state.frozen ?? {}).some((f) => f.cone === user) ? `${CONE_PREFIX}${user}` : void 0;
25
+ }
26
+ function usersLayout(state, envOf, root) {
27
+ const homeOfUser = (user) => user && user !== ROOT_USER ? homeOf(user) : ROOT_HOME;
28
+ return {
29
+ users: true,
30
+ scoopRoot: (folder) => homeOf(scoopUser(folder)),
31
+ workspace: (folder) => homeOf(scoopUser(folder)),
32
+ reports: (folder, cone) => `${homeOf(cone)}/.slicc/reports/${folder}`,
33
+ sessions: (persist, user) => persist === "tmp" ? `${SESSIONS.tmp}/${user ?? ROOT_USER}` : `${homeOfUser(user)}/sessions`,
34
+ homeOf: homeOfUser,
35
+ async ownDir(path, user, context) {
36
+ if (!user || user === ROOT_USER) return;
37
+ const dir = quote(path);
38
+ await root.exec(
39
+ `mkdir -p ${dir} && chown ${quote(user)} ${dir} && chmod 700 ${dir}`,
40
+ void 0,
41
+ context
42
+ );
43
+ },
44
+ async envFor(user) {
45
+ if (!user || user === ROOT_USER) return void 0;
46
+ const agentId = agentOfUser(state(), user);
47
+ return agentId ? envOf(agentId, homeOf(user)) : void 0;
48
+ }
49
+ };
50
+ }
51
+ export {
52
+ AGENT_DIR,
53
+ LEGACY,
54
+ ROOT_HOME,
55
+ ROOT_USER,
56
+ SCOOPS_ROOT,
57
+ SESSIONS,
58
+ SHARED_DIR,
59
+ agentOfUser,
60
+ usersLayout
61
+ };
62
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/layout.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport type { AgentsState } from './agents.ts';\nimport { CONE_PREFIX, homeOf, SCOOP_USER, scoopUser } from './kernel/accounts.ts';\nimport { quote } from './kernel/env.ts';\n\nexport const SCOOPS_ROOT = '/scoops';\nexport const SESSIONS = { tmp: '/tmp/agent-sessions', durable: '/home/sessions' };\nexport const ROOT_USER = 'root';\nexport const AGENT_DIR = '/etc/slicc/agent';\nexport const ROOT_HOME = '/root';\nexport const SHARED_DIR = '/var/lib/slicc/agent/shared';\n\nexport type Persist = 'tmp' | 'durable';\n\nexport interface Layout {\n readonly users: boolean;\n scoopRoot(folder: string): string;\n workspace(folder: string): string;\n reports(folder: string, cone: string): string;\n sessions(persist: Persist, user: string | null): string;\n homeOf(user: string | null): string;\n envFor(user: string | null, context: Context): Promise<ExecutionEnv | undefined>;\n ownDir(path: string, user: string | null, context: Context): Promise<void>;\n}\n\nexport const LEGACY: Layout = {\n users: false,\n scoopRoot: (folder) => `${SCOOPS_ROOT}/${folder}`,\n workspace: (folder) => `${SCOOPS_ROOT}/${folder}/workspace`,\n reports: (folder) => `${SCOOPS_ROOT}/${folder}/reports`,\n sessions: (persist) => SESSIONS[persist],\n homeOf: () => '/home',\n envFor: async () => undefined,\n ownDir: async () => undefined,\n};\n\nexport function agentOfUser(state: Readonly<AgentsState>, user: string): string | undefined {\n if (user.startsWith(SCOOP_USER)) {\n const folder = user.slice(SCOOP_USER.length);\n return Object.entries(state.scoops).find(([, record]) => record.folder === folder)?.[0];\n }\n return state.cones[user] || Object.values(state.frozen ?? {}).some((f) => f.cone === user)\n ? `${CONE_PREFIX}${user}`\n : undefined;\n}\n\nexport function usersLayout(\n state: () => Readonly<AgentsState>,\n envOf: (agentId: string, home: string) => Promise<ExecutionEnv>,\n root: Pick<ExecutionEnv, 'exec'>\n): Layout {\n const homeOfUser = (user: string | null) =>\n user && user !== ROOT_USER ? homeOf(user) : ROOT_HOME;\n return {\n users: true,\n scoopRoot: (folder) => homeOf(scoopUser(folder)),\n workspace: (folder) => homeOf(scoopUser(folder)),\n reports: (folder, cone) => `${homeOf(cone)}/.slicc/reports/${folder}`,\n sessions: (persist, user) =>\n persist === 'tmp' ? `${SESSIONS.tmp}/${user ?? ROOT_USER}` : `${homeOfUser(user)}/sessions`,\n homeOf: homeOfUser,\n async ownDir(path, user, context) {\n if (!user || user === ROOT_USER) return;\n const dir = quote(path);\n await root.exec(\n `mkdir -p ${dir} && chown ${quote(user)} ${dir} && chmod 700 ${dir}`,\n undefined,\n context\n );\n },\n async envFor(user) {\n if (!user || user === ROOT_USER) return undefined;\n const agentId = agentOfUser(state(), user);\n return agentId ? envOf(agentId, homeOf(user)) : undefined;\n },\n };\n}\n"],
5
+ "mappings": "AAGA,SAAS,aAAa,QAAQ,YAAY,iBAAiB;AAC3D,SAAS,aAAa;AAEf,MAAM,cAAc;AACpB,MAAM,WAAW,EAAE,KAAK,uBAAuB,SAAS,iBAAiB;AACzE,MAAM,YAAY;AAClB,MAAM,YAAY;AAClB,MAAM,YAAY;AAClB,MAAM,aAAa;AAenB,MAAM,SAAiB;AAAA,EAC5B,OAAO;AAAA,EACP,WAAW,CAAC,WAAW,GAAG,WAAW,IAAI,MAAM;AAAA,EAC/C,WAAW,CAAC,WAAW,GAAG,WAAW,IAAI,MAAM;AAAA,EAC/C,SAAS,CAAC,WAAW,GAAG,WAAW,IAAI,MAAM;AAAA,EAC7C,UAAU,CAAC,YAAY,SAAS,OAAO;AAAA,EACvC,QAAQ,MAAM;AAAA,EACd,QAAQ,YAAY;AAAA,EACpB,QAAQ,YAAY;AACtB;AAEO,SAAS,YAAY,OAA8B,MAAkC;AAC1F,MAAI,KAAK,WAAW,UAAU,GAAG;AAC/B,UAAM,SAAS,KAAK,MAAM,WAAW,MAAM;AAC3C,WAAO,OAAO,QAAQ,MAAM,MAAM,EAAE,KAAK,CAAC,CAAC,EAAE,MAAM,MAAM,OAAO,WAAW,MAAM,IAAI,CAAC;AAAA,EACxF;AACA,SAAO,MAAM,MAAM,IAAI,KAAK,OAAO,OAAO,MAAM,UAAU,CAAC,CAAC,EAAE,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,IACrF,GAAG,WAAW,GAAG,IAAI,KACrB;AACN;AAEO,SAAS,YACd,OACA,OACA,MACQ;AACR,QAAM,aAAa,CAAC,SAClB,QAAQ,SAAS,YAAY,OAAO,IAAI,IAAI;AAC9C,SAAO;AAAA,IACL,OAAO;AAAA,IACP,WAAW,CAAC,WAAW,OAAO,UAAU,MAAM,CAAC;AAAA,IAC/C,WAAW,CAAC,WAAW,OAAO,UAAU,MAAM,CAAC;AAAA,IAC/C,SAAS,CAAC,QAAQ,SAAS,GAAG,OAAO,IAAI,CAAC,mBAAmB,MAAM;AAAA,IACnE,UAAU,CAAC,SAAS,SAClB,YAAY,QAAQ,GAAG,SAAS,GAAG,IAAI,QAAQ,SAAS,KAAK,GAAG,WAAW,IAAI,CAAC;AAAA,IAClF,QAAQ;AAAA,IACR,MAAM,OAAO,MAAM,MAAM,SAAS;AAChC,UAAI,CAAC,QAAQ,SAAS,UAAW;AACjC,YAAM,MAAM,MAAM,IAAI;AACtB,YAAM,KAAK;AAAA,QACT,YAAY,GAAG,aAAa,MAAM,IAAI,CAAC,IAAI,GAAG,iBAAiB,GAAG;AAAA,QAClE;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,IACA,MAAM,OAAO,MAAM;AACjB,UAAI,CAAC,QAAQ,SAAS,UAAW,QAAO;AACxC,YAAM,UAAU,YAAY,MAAM,GAAG,IAAI;AACzC,aAAO,UAAU,MAAM,SAAS,OAAO,IAAI,CAAC,IAAI;AAAA,IAClD;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/licks/index.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, ConversationId, Harness, Registry } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport { type Agents, live } from '../agents.ts';\nimport type { Activity } from '../kernel/activity.ts';\nimport { cronTask, licksExtension } from './extension.ts';\nimport type { LickChannel, LickTarget } from './lick.ts';\nimport { createLicks, type Licks, type LicksHost } from './licks.ts';\nimport { createLickSources, type LickSources } from './sources.ts';\n\nexport interface LicksAgent {\n harness: Harness;\n root: Conversation;\n agents: Agents;\n cone(): Conversation;\n}\n\nconst UNTRUSTED = new Set<LickChannel>(['webhook']);\n\nexport function lickResolver(agent: LicksAgent) {\n return async (target: LickTarget, channel: LickChannel, context: Context) => {\n const cone = agent.cone();\n if (target === 'cone') return cone;\n if (target.startsWith('cone:'))\n return (await agent.agents.conversation(target.slice('cone:'.length), context)) ?? cone;\n if (UNTRUSTED.has(channel)) return cone;\n const scoop = agent.agents.state().scoops[target];\n if (!scoop || !live(scoop)) return cone;\n return (await agent.agents.conversation(target, context)) ?? cone;\n };\n}\n\nexport interface LicksSetup {\n licks: Licks;\n attach(\n agent: LicksAgent,\n options: {\n env: ExecutionEnv;\n home: string;\n now?: () => number;\n flushEvery?: number;\n activity?: Activity;\n }\n ): LickSources;\n}\n\nexport function setupLicks(registry: Registry): LicksSetup {\n let bind: (host: LicksHost) => void = () => undefined;\n const host = new Promise<LicksHost>((resolve) => {\n bind = resolve;\n });\n const licks = createLicks(host);\n const cron = cronTask(licks);\n const lookup = async (id: ConversationId, context: Context) => {\n const found = await (await host).harness.conversation(id, context);\n if (!found) throw new Error(`conversation ${id} is gone`);\n return found;\n };\n registry.install(licksExtension(licks, lookup, cron));\n return {\n licks,\n attach(agent, options) {\n const { harness, root } = agent;\n bind({ harness, resolve: lickResolver(agent) });\n return createLickSources({\n ...options,\n harness,\n root,\n cone: () => agent.cone(),\n licks,\n cron,\n });\n },\n };\n}\n\nexport type { Lick, LickChannel } from './lick.ts';\nexport type { LickEvent, Licks } from './licks.ts';\nexport type { LickSources, WebhookDelivery } from './sources.ts';\n"],
5
- "mappings": "AAGA,SAAsB,YAAY;AAElC,SAAS,UAAU,sBAAsB;AAEzC,SAAS,mBAA+C;AACxD,SAAS,yBAA2C;AASpD,MAAM,YAAY,oBAAI,IAAiB,CAAC,SAAS,CAAC;AAE3C,SAAS,aAAa,OAAmB;AAC9C,SAAO,OAAO,QAAoB,SAAsB,YAAqB;AAC3E,UAAM,OAAO,MAAM,KAAK;AACxB,QAAI,WAAW,OAAQ,QAAO;AAC9B,QAAI,OAAO,WAAW,OAAO;AAC3B,aAAQ,MAAM,MAAM,OAAO,aAAa,OAAO,MAAM,QAAQ,MAAM,GAAG,OAAO,KAAM;AACrF,QAAI,UAAU,IAAI,OAAO,EAAG,QAAO;AACnC,UAAM,QAAQ,MAAM,OAAO,MAAM,EAAE,OAAO,MAAM;AAChD,QAAI,CAAC,SAAS,CAAC,KAAK,KAAK,EAAG,QAAO;AACnC,WAAQ,MAAM,MAAM,OAAO,aAAa,QAAQ,OAAO,KAAM;AAAA,EAC/D;AACF;AAgBO,SAAS,WAAW,UAAgC;AACzD,MAAI,OAAkC,MAAM;AAC5C,QAAM,OAAO,IAAI,QAAmB,CAAC,YAAY;AAC/C,WAAO;AAAA,EACT,CAAC;AACD,QAAM,QAAQ,YAAY,IAAI;AAC9B,QAAM,OAAO,SAAS,KAAK;AAC3B,QAAM,SAAS,OAAO,IAAoB,YAAqB;AAC7D,UAAM,QAAQ,OAAO,MAAM,MAAM,QAAQ,aAAa,IAAI,OAAO;AACjE,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,gBAAgB,EAAE,UAAU;AACxD,WAAO;AAAA,EACT;AACA,WAAS,QAAQ,eAAe,OAAO,QAAQ,IAAI,CAAC;AACpD,SAAO;AAAA,IACL;AAAA,IACA,OAAO,OAAO,SAAS;AACrB,YAAM,EAAE,SAAS,KAAK,IAAI;AAC1B,WAAK,EAAE,SAAS,SAAS,aAAa,KAAK,EAAE,CAAC;AAC9C,aAAO,kBAAkB;AAAA,QACvB,GAAG;AAAA,QACH;AAAA,QACA;AAAA,QACA,MAAM,MAAM,MAAM,KAAK;AAAA,QACvB;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, ConversationId, Harness, Registry } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport { type Agents, live } from '../agents.ts';\nimport type { Activity } from '../kernel/activity.ts';\nimport { cronTask, licksExtension } from './extension.ts';\nimport type { LickChannel, LickTarget } from './lick.ts';\nimport { createLicks, type Licks, type LicksHost } from './licks.ts';\nimport { createLickSources, type LickDir, type LickSources } from './sources.ts';\n\nexport interface LicksAgent {\n harness: Harness;\n root: Conversation;\n agents: Agents;\n cone(): Conversation;\n}\n\nconst UNTRUSTED = new Set<LickChannel>(['webhook']);\n\nexport function lickResolver(agent: LicksAgent) {\n return async (target: LickTarget, channel: LickChannel, context: Context) => {\n const cone = agent.cone();\n if (target === 'cone') return cone;\n if (target.startsWith('cone:'))\n return (await agent.agents.conversation(target.slice('cone:'.length), context)) ?? cone;\n if (UNTRUSTED.has(channel)) return cone;\n const scoop = agent.agents.state().scoops[target];\n if (!scoop || !live(scoop)) return cone;\n return (await agent.agents.conversation(target, context)) ?? cone;\n };\n}\n\nexport interface LicksSetup {\n licks: Licks;\n attach(\n agent: LicksAgent,\n options: {\n env: ExecutionEnv;\n home: string;\n now?: () => number;\n flushEvery?: number;\n activity?: Activity;\n dirs?: () => Promise<readonly LickDir[]>;\n }\n ): LickSources;\n}\n\nexport function setupLicks(registry: Registry): LicksSetup {\n let bind: (host: LicksHost) => void = () => undefined;\n const host = new Promise<LicksHost>((resolve) => {\n bind = resolve;\n });\n const licks = createLicks(host);\n const cron = cronTask(licks);\n const lookup = async (id: ConversationId, context: Context) => {\n const found = await (await host).harness.conversation(id, context);\n if (!found) throw new Error(`conversation ${id} is gone`);\n return found;\n };\n registry.install(licksExtension(licks, lookup, cron));\n return {\n licks,\n attach(agent, options) {\n const { harness, root } = agent;\n bind({ harness, resolve: lickResolver(agent) });\n return createLickSources({\n ...options,\n harness,\n root,\n cone: () => agent.cone(),\n licks,\n cron,\n });\n },\n };\n}\n\nexport type { Lick, LickChannel } from './lick.ts';\nexport type { LickEvent, Licks } from './licks.ts';\nexport type { LickSources, WebhookDelivery } from './sources.ts';\n"],
5
+ "mappings": "AAGA,SAAsB,YAAY;AAElC,SAAS,UAAU,sBAAsB;AAEzC,SAAS,mBAA+C;AACxD,SAAS,yBAAyD;AASlE,MAAM,YAAY,oBAAI,IAAiB,CAAC,SAAS,CAAC;AAE3C,SAAS,aAAa,OAAmB;AAC9C,SAAO,OAAO,QAAoB,SAAsB,YAAqB;AAC3E,UAAM,OAAO,MAAM,KAAK;AACxB,QAAI,WAAW,OAAQ,QAAO;AAC9B,QAAI,OAAO,WAAW,OAAO;AAC3B,aAAQ,MAAM,MAAM,OAAO,aAAa,OAAO,MAAM,QAAQ,MAAM,GAAG,OAAO,KAAM;AACrF,QAAI,UAAU,IAAI,OAAO,EAAG,QAAO;AACnC,UAAM,QAAQ,MAAM,OAAO,MAAM,EAAE,OAAO,MAAM;AAChD,QAAI,CAAC,SAAS,CAAC,KAAK,KAAK,EAAG,QAAO;AACnC,WAAQ,MAAM,MAAM,OAAO,aAAa,QAAQ,OAAO,KAAM;AAAA,EAC/D;AACF;AAiBO,SAAS,WAAW,UAAgC;AACzD,MAAI,OAAkC,MAAM;AAC5C,QAAM,OAAO,IAAI,QAAmB,CAAC,YAAY;AAC/C,WAAO;AAAA,EACT,CAAC;AACD,QAAM,QAAQ,YAAY,IAAI;AAC9B,QAAM,OAAO,SAAS,KAAK;AAC3B,QAAM,SAAS,OAAO,IAAoB,YAAqB;AAC7D,UAAM,QAAQ,OAAO,MAAM,MAAM,QAAQ,aAAa,IAAI,OAAO;AACjE,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,gBAAgB,EAAE,UAAU;AACxD,WAAO;AAAA,EACT;AACA,WAAS,QAAQ,eAAe,OAAO,QAAQ,IAAI,CAAC;AACpD,SAAO;AAAA,IACL;AAAA,IACA,OAAO,OAAO,SAAS;AACrB,YAAM,EAAE,SAAS,KAAK,IAAI;AAC1B,WAAK,EAAE,SAAS,SAAS,aAAa,KAAK,EAAE,CAAC;AAC9C,aAAO,kBAAkB;AAAA,QACvB,GAAG;AAAA,QACH;AAAA,QACA;AAAA,QACA,MAAM,MAAM,MAAM,KAAK;AAAA,QACvB;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,8 +1,20 @@
1
1
  import { bootLicks } from "./boot.js";
2
- import { parseCrontab, parseWebhook } from "./config.js";
2
+ import {
3
+ parseCrontab,
4
+ parseWebhook
5
+ } from "./config.js";
3
6
  import { configFiles, Watches } from "./fswatch.js";
4
7
  import { reconcileCron } from "./schedules.js";
5
8
  const SLICC_DIR = ".slicc";
9
+ function scopedTarget(target, cone) {
10
+ return cone && target === "cone" ? `cone:${cone}` : target;
11
+ }
12
+ function scopedName(name, cone) {
13
+ return cone ? `${cone}:${name}` : name;
14
+ }
15
+ function scoped(entry, cone) {
16
+ return { ...entry, name: scopedName(entry.name, cone), target: scopedTarget(entry.target, cone) };
17
+ }
6
18
  function hash(text) {
7
19
  let value = 2166136261;
8
20
  for (let at = 0; at < text.length; at++) value = Math.imul(value ^ text.charCodeAt(at), 16777619);
@@ -11,7 +23,7 @@ function hash(text) {
11
23
  function shortPath(path, home) {
12
24
  return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
13
25
  }
14
- function configLick(error, home) {
26
+ function configLick(error, home, cone = null) {
15
27
  const file = shortPath(error.file, home);
16
28
  const channel = error.file.includes("/watches/") ? "fswatch" : error.file.includes("/webhooks/") ? "webhook" : "cron";
17
29
  const text = `${error.line ? `line ${error.line}: ` : ""}${error.error}`;
@@ -21,7 +33,7 @@ function configLick(error, home) {
21
33
  title: `Invalid ${file}`,
22
34
  text,
23
35
  body: "Ignored until the file is fixed.\nThe formats are in the slicc-agent README.",
24
- target: "cone",
36
+ target: scopedTarget("cone", cone),
25
37
  severity: "error",
26
38
  eventId: hash(`${file}
27
39
  ${text}`)
@@ -60,13 +72,23 @@ async function webhooksIn(files, dir, context) {
60
72
  }
61
73
  function createLickSources(options) {
62
74
  const { harness, env, licks, home } = options;
63
- const dir = `${home}/${SLICC_DIR}`;
64
75
  const now = options.now ?? Date.now;
65
76
  const files = configFiles(env);
66
77
  const deliver = (event, context) => licks.deliver(event, context);
67
- const watches = new Watches(env, deliver, home, options.activity);
78
+ const dirsOf = options.dirs ?? (() => [{ dir: `${home}/${SLICC_DIR}`, home, cone: null }]);
79
+ const watching = /* @__PURE__ */ new Map();
80
+ const watchesOf = (place) => {
81
+ let found = watching.get(place.dir);
82
+ if (!found) {
83
+ const retarget = (event, context) => deliver({ ...event, target: scopedTarget(event.target, place.cone) }, context);
84
+ found = new Watches(place.env ?? env, retarget, place.home, options.activity);
85
+ watching.set(place.dir, found);
86
+ }
87
+ return found;
88
+ };
68
89
  let webhooks = /* @__PURE__ */ new Map();
69
90
  let control;
91
+ let controlled = "";
70
92
  let reconcileTimer;
71
93
  let flushTimer;
72
94
  let closed = false;
@@ -78,14 +100,45 @@ function createLickSources(options) {
78
100
  };
79
101
  async function reconcileNow(context) {
80
102
  if (closed) return;
81
- const crontabFile = `${dir}/crontab`;
82
- const crontab = parseCrontab(crontabFile, await files.read(crontabFile, context) ?? "");
83
- await reconcileCron({ ...options, now }, crontab.entries, context);
84
- const watchErrors = await watches.reconcile(`${dir}/watches`, context);
85
- const found = await webhooksIn(files, `${dir}/webhooks`, context);
86
- webhooks = found.entries;
87
- const errors = [...crontab.errors, ...watchErrors, ...found.errors];
88
- for (const error of errors) await deliver(configLick(error, home), context);
103
+ const places = await dirsOf();
104
+ const entries = [];
105
+ const errors = [];
106
+ const hooks = /* @__PURE__ */ new Map();
107
+ for (const place of places) {
108
+ const crontabFile = `${place.dir}/crontab`;
109
+ const crontab = parseCrontab(crontabFile, await files.read(crontabFile, context) ?? "");
110
+ entries.push(...crontab.entries.map((entry) => scoped(entry, place.cone)));
111
+ const watchErrors = await watchesOf(place).reconcile(`${place.dir}/watches`, context);
112
+ const found = await webhooksIn(files, `${place.dir}/webhooks`, context);
113
+ for (const entry of found.entries.values()) {
114
+ const named = scoped(entry, place.cone);
115
+ hooks.set(named.name, named);
116
+ }
117
+ for (const error of [...crontab.errors, ...watchErrors, ...found.errors])
118
+ errors.push({ error, place });
119
+ }
120
+ for (const [dir, watches] of [...watching])
121
+ if (!places.some((place) => place.dir === dir)) {
122
+ watching.delete(dir);
123
+ await watches.close(context);
124
+ }
125
+ await reconcileCron({ ...options, now }, entries, context);
126
+ webhooks = hooks;
127
+ for (const { error, place } of errors)
128
+ await deliver(configLick(error, place.home, place.cone), context);
129
+ await watchControl(places, context);
130
+ }
131
+ async function watchControl(places, context) {
132
+ const key = places.map((place) => place.dir).join("\n");
133
+ if (key === controlled || closed) return;
134
+ controlled = key;
135
+ await control?.close(context);
136
+ const result = await env.watch(
137
+ places.map((place) => ({ path: place.dir, recursive: true })),
138
+ () => scheduleReconcile(context),
139
+ context
140
+ );
141
+ control = result.ok ? result.value : void 0;
89
142
  }
90
143
  const reconcile = (context) => serial(() => reconcileNow(context));
91
144
  function scheduleReconcile(context) {
@@ -101,12 +154,6 @@ function createLickSources(options) {
101
154
  async start(context) {
102
155
  await licks.flush(context);
103
156
  await reconcile(context);
104
- const result = await env.watch(
105
- [{ path: dir, recursive: true }],
106
- () => scheduleReconcile(context),
107
- context
108
- );
109
- if (result.ok) control = result.value;
110
157
  flushTimer = setInterval(() => {
111
158
  void flushIfWaiting(context).catch(() => void 0);
112
159
  }, options.flushEvery ?? 2e3);
@@ -124,7 +171,7 @@ function createLickSources(options) {
124
171
  clearTimeout(reconcileTimer);
125
172
  clearInterval(flushTimer);
126
173
  await control?.close(context);
127
- await watches.close(context);
174
+ for (const watches of watching.values()) await watches.close(context);
128
175
  }
129
176
  };
130
177
  }
@@ -133,6 +180,8 @@ export {
133
180
  configLick,
134
181
  createLickSources,
135
182
  hash,
183
+ scopedName,
184
+ scopedTarget,
136
185
  shortPath,
137
186
  webhookLick
138
187
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/licks/sources.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, Harness } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv, FileWatcher } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport { type BootFacts, bootLicks } from './boot.ts';\nimport { type ConfigError, parseCrontab, parseWebhook, type WebhookEntry } from './config.ts';\nimport type { cronTask } from './extension.ts';\nimport { type ConfigFiles, configFiles, Watches } from './fswatch.ts';\nimport type { LickChannel } from './lick.ts';\nimport type { LickEvent, Licks } from './licks.ts';\nimport { reconcileCron } from './schedules.ts';\n\nexport const SLICC_DIR = '.slicc';\n\nexport interface LickSourcesOptions {\n harness: Harness;\n root: Conversation;\n cone: () => Conversation;\n env: ExecutionEnv;\n home: string;\n licks: Licks;\n cron: ReturnType<typeof cronTask>;\n now?: () => number;\n flushEvery?: number;\n activity?: Activity;\n}\n\nexport interface WebhookDelivery {\n id?: string;\n headers?: Record<string, string>;\n body: unknown;\n}\n\nexport interface LickSources {\n start(context: Context): Promise<void>;\n reconcile(context: Context): Promise<void>;\n boot(facts: BootFacts, context: Context): Promise<void>;\n webhook(name: string, delivery: WebhookDelivery, context: Context): Promise<boolean>;\n close(context: Context): Promise<void>;\n}\n\nexport function hash(text: string): string {\n let value = 2166136261;\n for (let at = 0; at < text.length; at++) value = Math.imul(value ^ text.charCodeAt(at), 16777619);\n return (value >>> 0).toString(36);\n}\n\nexport function shortPath(path: string, home: string): string {\n return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;\n}\n\nexport function configLick(error: ConfigError, home: string): LickEvent {\n const file = shortPath(error.file, home);\n const channel: LickChannel = error.file.includes('/watches/')\n ? 'fswatch'\n : error.file.includes('/webhooks/')\n ? 'webhook'\n : 'cron';\n const text = `${error.line ? `line ${error.line}: ` : ''}${error.error}`;\n return {\n channel,\n source: `config:${file}`,\n title: `Invalid ${file}`,\n text,\n body: 'Ignored until the file is fixed.\\nThe formats are in the slicc-agent README.',\n target: 'cone',\n severity: 'error',\n eventId: hash(`${file}\\n${text}`),\n };\n}\n\nexport function webhookLick(\n name: string,\n entry: WebhookEntry,\n delivery: WebhookDelivery\n): LickEvent {\n const payload =\n typeof delivery.body === 'string' ? delivery.body : JSON.stringify(delivery.body, null, 2);\n const headers = Object.entries(delivery.headers ?? {})\n .map(([key, value]) => `${key}: ${value}`)\n .join('\\n');\n return {\n channel: 'webhook',\n source: name,\n title: name,\n text: entry.message || `Webhook ${name} received a request`,\n body: [headers, payload].filter(Boolean).join('\\n\\n'),\n target: entry.target,\n coalesce: false,\n ...(delivery.id ? { eventId: delivery.id } : {}),\n ...(entry.triage ? { triage: entry.triage } : {}),\n };\n}\n\nasync function webhooksIn(\n files: ConfigFiles,\n dir: string,\n context: Context\n): Promise<{ entries: Map<string, WebhookEntry>; errors: ConfigError[] }> {\n const errors: ConfigError[] = [];\n const into = new Map<string, WebhookEntry>();\n for (const name of await files.list(dir, context)) {\n const file = `${dir}/${name}`;\n const json = await files.read(file, context);\n if (json === undefined) continue;\n try {\n const entry = parseWebhook(file, json);\n into.set(entry.name, entry);\n } catch (error) {\n errors.push({ file, error: (error as Error).message });\n }\n }\n return { entries: into, errors };\n}\n\nexport function createLickSources(options: LickSourcesOptions): LickSources {\n const { harness, env, licks, home } = options;\n const dir = `${home}/${SLICC_DIR}`;\n const now = options.now ?? Date.now;\n const files = configFiles(env);\n const deliver = (event: LickEvent, context: Context) => licks.deliver(event, context);\n const watches = new Watches(env, deliver, home, options.activity);\n let webhooks = new Map<string, WebhookEntry>();\n let control: FileWatcher | undefined;\n let reconcileTimer: ReturnType<typeof setTimeout> | undefined;\n let flushTimer: ReturnType<typeof setInterval> | undefined;\n let closed = false;\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n\n async function reconcileNow(context: Context): Promise<void> {\n if (closed) return;\n const crontabFile = `${dir}/crontab`;\n const crontab = parseCrontab(crontabFile, (await files.read(crontabFile, context)) ?? '');\n await reconcileCron({ ...options, now }, crontab.entries, context);\n const watchErrors = await watches.reconcile(`${dir}/watches`, context);\n const found = await webhooksIn(files, `${dir}/webhooks`, context);\n webhooks = found.entries;\n const errors = [...crontab.errors, ...watchErrors, ...found.errors];\n for (const error of errors) await deliver(configLick(error, home), context);\n }\n\n const reconcile = (context: Context) => serial(() => reconcileNow(context));\n\n function scheduleReconcile(context: Context): void {\n clearTimeout(reconcileTimer);\n reconcileTimer = setTimeout(() => {\n void reconcile(context).catch(() => undefined);\n }, 300);\n }\n\n async function flushIfWaiting(context: Context): Promise<void> {\n if (await licks.pending(context)) await licks.flush(context);\n }\n\n return {\n async start(context) {\n await licks.flush(context);\n await reconcile(context);\n const result = await env.watch(\n [{ path: dir, recursive: true }],\n () => scheduleReconcile(context),\n context\n );\n if (result.ok) control = result.value;\n flushTimer = setInterval(() => {\n void flushIfWaiting(context).catch(() => undefined);\n }, options.flushEvery ?? 2000);\n },\n reconcile,\n boot: (facts, context) => bootLicks(harness, options.cone(), deliver, facts, context),\n async webhook(name, delivery, context) {\n const entry = webhooks.get(name);\n if (!entry) return false;\n await deliver(webhookLick(name, entry, delivery), context);\n return true;\n },\n async close(context) {\n closed = true;\n clearTimeout(reconcileTimer);\n clearInterval(flushTimer);\n await control?.close(context);\n await watches.close(context);\n },\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, Harness } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv, FileWatcher } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport { type BootFacts, bootLicks } from './boot.ts';\nimport {\n type ConfigError,\n type CronEntry,\n parseCrontab,\n parseWebhook,\n type WebhookEntry,\n} from './config.ts';\nimport type { cronTask } from './extension.ts';\nimport { type ConfigFiles, configFiles, Watches } from './fswatch.ts';\nimport type { LickChannel, LickTarget } from './lick.ts';\nimport type { LickEvent, Licks } from './licks.ts';\nimport { reconcileCron } from './schedules.ts';\n\nexport const SLICC_DIR = '.slicc';\n\nexport interface LickSourcesOptions {\n harness: Harness;\n root: Conversation;\n cone: () => Conversation;\n env: ExecutionEnv;\n home: string;\n licks: Licks;\n cron: ReturnType<typeof cronTask>;\n now?: () => number;\n flushEvery?: number;\n activity?: Activity;\n dirs?: () => readonly LickDir[] | Promise<readonly LickDir[]>;\n}\n\nexport interface LickDir {\n dir: string;\n home: string;\n cone: string | null;\n env?: ExecutionEnv;\n}\n\nexport function scopedTarget(target: LickTarget, cone: string | null): LickTarget {\n return cone && target === 'cone' ? (`cone:${cone}` as LickTarget) : target;\n}\n\nexport function scopedName(name: string, cone: string | null): string {\n return cone ? `${cone}:${name}` : name;\n}\n\nfunction scoped<T extends { name: string; target: LickTarget }>(entry: T, cone: string | null): T {\n return { ...entry, name: scopedName(entry.name, cone), target: scopedTarget(entry.target, cone) };\n}\n\nexport interface WebhookDelivery {\n id?: string;\n headers?: Record<string, string>;\n body: unknown;\n}\n\nexport interface LickSources {\n start(context: Context): Promise<void>;\n reconcile(context: Context): Promise<void>;\n boot(facts: BootFacts, context: Context): Promise<void>;\n webhook(name: string, delivery: WebhookDelivery, context: Context): Promise<boolean>;\n close(context: Context): Promise<void>;\n}\n\nexport function hash(text: string): string {\n let value = 2166136261;\n for (let at = 0; at < text.length; at++) value = Math.imul(value ^ text.charCodeAt(at), 16777619);\n return (value >>> 0).toString(36);\n}\n\nexport function shortPath(path: string, home: string): string {\n return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;\n}\n\nexport function configLick(\n error: ConfigError,\n home: string,\n cone: string | null = null\n): LickEvent {\n const file = shortPath(error.file, home);\n const channel: LickChannel = error.file.includes('/watches/')\n ? 'fswatch'\n : error.file.includes('/webhooks/')\n ? 'webhook'\n : 'cron';\n const text = `${error.line ? `line ${error.line}: ` : ''}${error.error}`;\n return {\n channel,\n source: `config:${file}`,\n title: `Invalid ${file}`,\n text,\n body: 'Ignored until the file is fixed.\\nThe formats are in the slicc-agent README.',\n target: scopedTarget('cone', cone),\n severity: 'error',\n eventId: hash(`${file}\\n${text}`),\n };\n}\n\nexport function webhookLick(\n name: string,\n entry: WebhookEntry,\n delivery: WebhookDelivery\n): LickEvent {\n const payload =\n typeof delivery.body === 'string' ? delivery.body : JSON.stringify(delivery.body, null, 2);\n const headers = Object.entries(delivery.headers ?? {})\n .map(([key, value]) => `${key}: ${value}`)\n .join('\\n');\n return {\n channel: 'webhook',\n source: name,\n title: name,\n text: entry.message || `Webhook ${name} received a request`,\n body: [headers, payload].filter(Boolean).join('\\n\\n'),\n target: entry.target,\n coalesce: false,\n ...(delivery.id ? { eventId: delivery.id } : {}),\n ...(entry.triage ? { triage: entry.triage } : {}),\n };\n}\n\nasync function webhooksIn(\n files: ConfigFiles,\n dir: string,\n context: Context\n): Promise<{ entries: Map<string, WebhookEntry>; errors: ConfigError[] }> {\n const errors: ConfigError[] = [];\n const into = new Map<string, WebhookEntry>();\n for (const name of await files.list(dir, context)) {\n const file = `${dir}/${name}`;\n const json = await files.read(file, context);\n if (json === undefined) continue;\n try {\n const entry = parseWebhook(file, json);\n into.set(entry.name, entry);\n } catch (error) {\n errors.push({ file, error: (error as Error).message });\n }\n }\n return { entries: into, errors };\n}\n\nexport function createLickSources(options: LickSourcesOptions): LickSources {\n const { harness, env, licks, home } = options;\n const now = options.now ?? Date.now;\n const files = configFiles(env);\n const deliver = (event: LickEvent, context: Context) => licks.deliver(event, context);\n const dirsOf = options.dirs ?? (() => [{ dir: `${home}/${SLICC_DIR}`, home, cone: null }]);\n const watching = new Map<string, Watches>();\n const watchesOf = (place: LickDir) => {\n let found = watching.get(place.dir);\n if (!found) {\n const retarget = (event: LickEvent, context: Context) =>\n deliver({ ...event, target: scopedTarget(event.target, place.cone) }, context);\n found = new Watches(place.env ?? env, retarget, place.home, options.activity);\n watching.set(place.dir, found);\n }\n return found;\n };\n let webhooks = new Map<string, WebhookEntry>();\n let control: FileWatcher | undefined;\n let controlled = '';\n let reconcileTimer: ReturnType<typeof setTimeout> | undefined;\n let flushTimer: ReturnType<typeof setInterval> | undefined;\n let closed = false;\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n\n async function reconcileNow(context: Context): Promise<void> {\n if (closed) return;\n const places = await dirsOf();\n const entries: CronEntry[] = [];\n const errors: { error: ConfigError; place: LickDir }[] = [];\n const hooks = new Map<string, WebhookEntry>();\n for (const place of places) {\n const crontabFile = `${place.dir}/crontab`;\n const crontab = parseCrontab(crontabFile, (await files.read(crontabFile, context)) ?? '');\n entries.push(...crontab.entries.map((entry) => scoped(entry, place.cone)));\n const watchErrors = await watchesOf(place).reconcile(`${place.dir}/watches`, context);\n const found = await webhooksIn(files, `${place.dir}/webhooks`, context);\n for (const entry of found.entries.values()) {\n const named = scoped(entry, place.cone);\n hooks.set(named.name, named);\n }\n for (const error of [...crontab.errors, ...watchErrors, ...found.errors])\n errors.push({ error, place });\n }\n for (const [dir, watches] of [...watching])\n if (!places.some((place) => place.dir === dir)) {\n watching.delete(dir);\n await watches.close(context);\n }\n await reconcileCron({ ...options, now }, entries, context);\n webhooks = hooks;\n for (const { error, place } of errors)\n await deliver(configLick(error, place.home, place.cone), context);\n await watchControl(places, context);\n }\n\n async function watchControl(places: readonly LickDir[], context: Context): Promise<void> {\n const key = places.map((place) => place.dir).join('\\n');\n if (key === controlled || closed) return;\n controlled = key;\n await control?.close(context);\n const result = await env.watch(\n places.map((place) => ({ path: place.dir, recursive: true })),\n () => scheduleReconcile(context),\n context\n );\n control = result.ok ? result.value : undefined;\n }\n\n const reconcile = (context: Context) => serial(() => reconcileNow(context));\n\n function scheduleReconcile(context: Context): void {\n clearTimeout(reconcileTimer);\n reconcileTimer = setTimeout(() => {\n void reconcile(context).catch(() => undefined);\n }, 300);\n }\n\n async function flushIfWaiting(context: Context): Promise<void> {\n if (await licks.pending(context)) await licks.flush(context);\n }\n\n return {\n async start(context) {\n await licks.flush(context);\n await reconcile(context);\n flushTimer = setInterval(() => {\n void flushIfWaiting(context).catch(() => undefined);\n }, options.flushEvery ?? 2000);\n },\n reconcile,\n boot: (facts, context) => bootLicks(harness, options.cone(), deliver, facts, context),\n async webhook(name, delivery, context) {\n const entry = webhooks.get(name);\n if (!entry) return false;\n await deliver(webhookLick(name, entry, delivery), context);\n return true;\n },\n async close(context) {\n closed = true;\n clearTimeout(reconcileTimer);\n clearInterval(flushTimer);\n await control?.close(context);\n for (const watches of watching.values()) await watches.close(context);\n },\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/dist/main.js CHANGED
@@ -21,9 +21,11 @@ import { lateGelatiere } from "./gelatiere/index.js";
21
21
  import { hostAgent } from "./host.js";
22
22
  import { imageReads } from "./images/index.js";
23
23
  import { createActivity } from "./kernel/activity.js";
24
+ import { agentUsers, warnSupport } from "./kernel/agent-users.js";
24
25
  import { HOME, kernelEnvironment, SliccKernelEnv } from "./kernel/env.js";
25
26
  import { processGroups } from "./kernel/groups.js";
26
27
  import { adminOver, userClients } from "./kernel/users.js";
28
+ import { AGENT_DIR, ROOT_HOME } from "./layout.js";
27
29
  import { setupLicks } from "./licks/index.js";
28
30
  import { lickTriage } from "./licks/triage.js";
29
31
  import { setupMcp } from "./mcp/index.js";
@@ -68,13 +70,17 @@ async function attached(ports, options) {
68
70
  const attach = options.attach ?? attachKernel;
69
71
  const { kernel, admin } = await ports;
70
72
  const client = await attach(kernel);
71
- return { client, users: usersOf(admin, options.users, attach) };
73
+ const users = usersOf(admin, options.users, attach);
74
+ await warnSupport(users, client, options.users);
75
+ const home = new SliccKernelEnv(client, { cwd: HOME });
76
+ await privateStore(home);
77
+ return { client, users, home, at: { home: HOME, ...users ? { agentDir: AGENT_DIR } : {} } };
72
78
  }
73
79
  function privateStore(env) {
74
80
  return env.exec(`mkdir -p ${STORE_DIR} && chmod 700 ${STORE_DIR}`, void 0, BACKGROUND_CONTEXT);
75
81
  }
76
- async function codemodeTools(client, options, native, extra) {
77
- const settings = await client.fs.readFile(`${HOME}/.pi/agent/settings.json`).catch(() => void 0);
82
+ async function codemodeTools(client, options, native, extra, dir = `${HOME}/.pi/agent`) {
83
+ const settings = await client.fs.readFile(`${dir}/settings.json`).catch(() => void 0);
78
84
  if (disabledBySettings(settings === void 0 ? void 0 : new TextDecoder().decode(settings)))
79
85
  return [];
80
86
  const sandbox = sandboxFactory(
@@ -104,42 +110,50 @@ async function secretsFor(home, vault) {
104
110
  await names.refresh(BACKGROUND_CONTEXT);
105
111
  return names;
106
112
  }
113
+ async function modelsOf(options) {
114
+ const credentials = await (options.credentials ?? (() => EncryptedCredentialStore.open()))();
115
+ const providers = options.providers ?? sliccProviders();
116
+ return { credentials, providers, models: createSliccModels(credentials, providers) };
117
+ }
118
+ function baseRegistry(facts) {
119
+ const registry = createRegistry();
120
+ registry.install(CodingTools);
121
+ registry.install(sliccPrompt(facts));
122
+ const read = CodingTools.tools.find(
123
+ (tool) => tool.name === "read"
124
+ );
125
+ registry.install(imageReads(read));
126
+ return registry;
127
+ }
128
+ async function factsOf(client, native) {
129
+ return {
130
+ version: await agentVersion((url) => native(url)),
131
+ commands: await commandsOnPath(client),
132
+ transport: transportName(client.transport),
133
+ browser: await browserConnected(client)
134
+ };
135
+ }
107
136
  function routeFetch(scope, client) {
108
137
  const native = scope.fetch.bind(scope);
109
138
  const loopback = client.loopbackFetch?.bind(client);
110
139
  scope.fetch = transportFetch(client.transport, scope.location.origin, native, loopback);
111
140
  return native;
112
141
  }
142
+ async function prepare(scope, kernel, options) {
143
+ const names = await secretsFor(kernel.home, kernel.vault);
144
+ const native = routeFetch(scope, kernel.client);
145
+ const facts = await factsOf(kernel.client, native);
146
+ return { names, native, facts, ...await modelsOf(options), registry: baseRegistry(facts) };
147
+ }
113
148
  async function start(scope, ports, options, vault) {
114
- const { client, users } = await attached(ports, options);
115
- const home = new SliccKernelEnv(client, { cwd: HOME });
116
- await privateStore(home);
117
- const names = await secretsFor(home, vault);
118
- const native = routeFetch(scope, client);
119
- const facts = {
120
- version: await agentVersion((url) => native(url)),
121
- commands: await commandsOnPath(client),
122
- transport: transportName(client.transport),
123
- browser: await browserConnected(client)
124
- };
125
- const credentials = await (options.credentials ?? (() => EncryptedCredentialStore.open()))();
126
- const providers = options.providers ?? sliccProviders();
127
- const models = createSliccModels(credentials, providers);
128
- const registry = createRegistry();
129
- registry.install(CodingTools);
130
- registry.install(sliccPrompt(facts));
131
- registry.install(
132
- imageReads(
133
- CodingTools.tools.find(
134
- (tool) => tool.name === "read"
135
- )
136
- )
137
- );
149
+ const { client, users, home, at } = await attached(ports, options);
150
+ const ready = await prepare(scope, { client, home, vault }, options);
151
+ const { names, native, facts, credentials, providers, models, registry } = ready;
138
152
  const memory = setupMemory(registry);
139
153
  const coding = CodingTools.tools;
140
154
  registry.install(browserGuard([scope.location.origin], coding));
141
155
  const mcp = setupMcp(registry);
142
- const codemode = await codemodeTools(client, options, native, mcp.codemode);
156
+ const codemode = await codemodeTools(client, options, native, mcp.codemode, at.agentDir);
143
157
  registry.install(codemodeExtension(codemode));
144
158
  const licks = setupLicks(registry);
145
159
  licks.licks.triage(lickTriage(models));
@@ -150,29 +164,43 @@ async function start(scope, ports, options, vault) {
150
164
  const groups = processGroups(client);
151
165
  const model = options.model ?? DEFAULT_MODEL;
152
166
  let opened;
167
+ const people = users ? agentUsers(users, client, home, () => opened?.agents) : void 0;
153
168
  const environment = kernelEnvironment(client, {
154
169
  activity,
155
170
  groups,
156
171
  identify: identity(() => opened?.agents, model),
157
172
  scrub: vault,
158
- spawn: shellGrants(names)
173
+ spawn: shellGrants(names),
174
+ ...people ? { route: people.route } : {}
159
175
  });
160
176
  const agent = await openRepinned(options, { models, model, registry, env: environment });
161
177
  opened = agent;
162
- const sources = licks.attach(agent, { env: home, home: HOME, activity });
178
+ await people?.start(agent.agents);
179
+ const sources = licks.attach(agent, {
180
+ env: home,
181
+ home: HOME,
182
+ activity,
183
+ ...people ? { dirs: () => people.lickDirs() } : {}
184
+ });
163
185
  await sources.start(BACKGROUND_CONTEXT);
164
186
  await sources.boot(
165
187
  { version: facts.version, boot: await kernelBoot(client) },
166
188
  BACKGROUND_CONTEXT
167
189
  );
168
190
  const skillsRuntime = await skills.attach(
169
- { env: home, home: HOME, assets: options.assets ?? packageAssets(), reloadMs: RELOAD_MS },
191
+ {
192
+ env: home,
193
+ ...at,
194
+ assets: options.assets ?? packageAssets(),
195
+ reloadMs: RELOAD_MS,
196
+ ...people ? { people: people.people } : {}
197
+ },
170
198
  BACKGROUND_CONTEXT
171
199
  );
172
200
  const mcpRuntime = await mcp.attach(
173
201
  {
174
202
  env: home,
175
- home: HOME,
203
+ ...at,
176
204
  credentials,
177
205
  licks: licks.licks,
178
206
  fetch: scope.fetch,
@@ -193,17 +221,20 @@ async function start(scope, ports, options, vault) {
193
221
  licks: licks.licks,
194
222
  env: home,
195
223
  assets: options.assets ?? packageAssets(),
196
- reloadMs: RELOAD_MS
224
+ reloadMs: RELOAD_MS,
225
+ ...people?.sprinkles() ?? {}
197
226
  },
198
227
  BACKGROUND_CONTEXT
199
228
  );
229
+ people?.follow([sources, skillsRuntime, sprinkles]);
200
230
  let roles;
201
231
  const memoryRuntime = await memory.attach(
202
232
  {
203
233
  harness: agent.harness,
204
234
  agents: agent.agents,
205
235
  env: home,
206
- home: HOME,
236
+ ...at,
237
+ ...people ? { home: ROOT_HOME } : {},
207
238
  reloadMs: RELOAD_MS,
208
239
  roles: async (using) => roles?.roles(using)
209
240
  },
@@ -214,7 +245,8 @@ async function start(scope, ports, options, vault) {
214
245
  agents: agent.agents,
215
246
  env: home,
216
247
  extract: memory.extract,
217
- title: titler(models, settingsText(home, HOME))
248
+ title: titler(models, settingsText(home, HOME, at.agentDir)),
249
+ ...people ? { layout: people.layout } : {}
218
250
  });
219
251
  licks.licks.guard(freezer.frozenTarget);
220
252
  const settings = await createAgentSettings(models, credentials, providers, { mcp: mcpRuntime });
@@ -225,7 +257,8 @@ async function start(scope, ports, options, vault) {
225
257
  agents: agent.agents,
226
258
  groups,
227
259
  env: home,
228
- home: HOME,
260
+ ...at,
261
+ ...people ? { layout: people.layout, spools: people.spools, enforced: people.enforced } : {},
229
262
  alive: alive(client),
230
263
  reads: skillsRuntime.dirs,
231
264
  memory: memoryRuntime.command,
@@ -244,6 +277,7 @@ async function start(scope, ports, options, vault) {
244
277
  licks: licks.licks,
245
278
  env: home,
246
279
  home: HOME,
280
+ ...await people?.gelatiere(home),
247
281
  scoops: scoops.scoops,
248
282
  roles: runtime.roles,
249
283
  assets: options.assets ?? packageAssets(),