neko-harness 0.4.9 → 0.5.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.
@@ -43,8 +43,8 @@
43
43
  href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='26' fill='%23ffb7c5'/%3E%3Ctext x='50' y='70' font-size='58' text-anchor='middle' fill='%23ffffff' font-family='sans-serif' font-weight='bold'%3En%3C/text%3E%3C/svg%3E"
44
44
  />
45
45
  <title>neko harness</title>
46
- <script type="module" crossorigin src="/assets/index-LHUxaMG9.js"></script>
47
- <link rel="stylesheet" crossorigin href="/assets/index-CP-ffuep.css">
46
+ <script type="module" crossorigin src="/assets/index-v4Ct9JMn.js"></script>
47
+ <link rel="stylesheet" crossorigin href="/assets/index-DCZeTvcw.css">
48
48
  </head>
49
49
  <body>
50
50
  <div id="root"></div>
package/dist/gateway.js CHANGED
@@ -17022,14 +17022,20 @@ function normalizeEnabledProviders(saved) {
17022
17022
  // packages/core/src/credentials.ts
17023
17023
  import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
17024
17024
  var STALE_LOCK_MS = 30000;
17025
+ var LOCK_ATTEMPTS = 100;
17026
+ var LOCK_RETRY_MS = 25;
17025
17027
  var DEFAULT_CREDENTIALS_PATH = join2(homedir2(), ".neko", "credentials.json");
17026
17028
 
17027
17029
  class FileCredentialStore {
17028
17030
  path;
17029
17031
  #renameDeps;
17030
- constructor(path = DEFAULT_CREDENTIALS_PATH, renameDeps = {}) {
17032
+ #lockAttempts;
17033
+ #lockRetryMs;
17034
+ constructor(path = DEFAULT_CREDENTIALS_PATH, renameDeps = {}, lock = {}) {
17031
17035
  this.path = path;
17032
17036
  this.#renameDeps = renameDeps;
17037
+ this.#lockAttempts = lock.lockAttempts ?? LOCK_ATTEMPTS;
17038
+ this.#lockRetryMs = lock.lockRetryMs ?? LOCK_RETRY_MS;
17033
17039
  }
17034
17040
  async#read() {
17035
17041
  try {
@@ -17065,7 +17071,7 @@ class FileCredentialStore {
17065
17071
  lock = await open(lockPath, "wx", 384);
17066
17072
  break;
17067
17073
  } catch (error) {
17068
- if (error.code !== "EEXIST" || attempt >= 100)
17074
+ if (error.code !== "EEXIST" || attempt >= this.#lockAttempts)
17069
17075
  throw new Error("Credential storage is busy or unavailable.");
17070
17076
  if (!reclaimed) {
17071
17077
  const info = await stat(lockPath).catch(() => {
@@ -17077,7 +17083,7 @@ class FileCredentialStore {
17077
17083
  continue;
17078
17084
  }
17079
17085
  }
17080
- await new Promise((resolve) => setTimeout(resolve, 25));
17086
+ await new Promise((resolve) => setTimeout(resolve, this.#lockRetryMs));
17081
17087
  }
17082
17088
  }
17083
17089
  const temporary = `${this.path}.${crypto.randomUUID()}.tmp`;
@@ -17235,6 +17241,9 @@ function accessibleWorkspace(directory) {
17235
17241
  }
17236
17242
  return path;
17237
17243
  }
17244
+ var WORKSPACE_UNKNOWN = "WORKSPACE_UNKNOWN: This workspace is not in the list.";
17245
+ var WORKSPACE_LAST = "WORKSPACE_LAST: Keep at least one workspace.";
17246
+ var WORKSPACE_BUSY = "WORKSPACE_BUSY: Stop or finish the running sessions of this workspace first.";
17238
17247
 
17239
17248
  // packages/core/src/projection.ts
17240
17249
  function userText(content) {
@@ -22612,6 +22621,14 @@ var WorkspacesDoc = defineDoc8({
22612
22621
  fork: "initial",
22613
22622
  initial: () => ({ items: [] })
22614
22623
  });
22624
+ var RemovedWorkspacesDoc = defineDoc8({
22625
+ kind: "app.neko.workspaces-removed",
22626
+ version: 1,
22627
+ scope: "conversation",
22628
+ history: "latest",
22629
+ fork: "initial",
22630
+ initial: () => ({ paths: [] })
22631
+ });
22615
22632
  var SearchSettingsDoc = defineDoc8({
22616
22633
  kind: "app.neko.search-settings",
22617
22634
  version: 1,
@@ -22830,20 +22847,57 @@ async function createNekoRuntime(options = {}) {
22830
22847
  contextState = resolveContextSettings(options.contextSettings, savedContext);
22831
22848
  return contextState;
22832
22849
  };
22833
- const registerWorkspace = async (path) => {
22850
+ const removedWorkspaces = async () => new Set((await harness.snapshot(RemovedWorkspacesDoc, conversation.id, context))?.paths ?? []);
22851
+ const registerWorkspace = async (path, explicit) => {
22834
22852
  await harness.commit(async (tx) => {
22835
22853
  const doc = await tx.doc(WorkspacesDoc, conversation.id);
22854
+ const removed = await tx.doc(RemovedWorkspacesDoc, conversation.id);
22855
+ if (removed.paths.includes(path)) {
22856
+ if (!explicit)
22857
+ return;
22858
+ removed.paths = removed.paths.filter((entry) => entry !== path);
22859
+ }
22836
22860
  if (!doc.items.some((item) => item.path === path))
22837
22861
  doc.items.push({ path, createdAt: Date.now() });
22838
22862
  }, context);
22863
+ };
22864
+ const addWorkspace = async (input) => {
22865
+ const path = accessibleWorkspace(input);
22866
+ await registerWorkspace(path, true);
22839
22867
  return (await harness.snapshot(WorkspacesDoc, conversation.id, context)).items.find((item) => item.path === path);
22840
22868
  };
22841
- const addWorkspace = async (path) => registerWorkspace(accessibleWorkspace(path));
22842
22869
  for (const record of [...existing, { id: conversation.id }]) {
22843
22870
  const target = await harness.conversation(record.id, context);
22844
22871
  if (target)
22845
- await registerWorkspace(savedWorkspace((await target.agent(context)).cwd));
22846
- }
22872
+ await registerWorkspace(savedWorkspace((await target.agent(context)).cwd), false);
22873
+ }
22874
+ await registerWorkspace(defaultCwd, true);
22875
+ const removeWorkspace = async (path) => {
22876
+ const items = (await harness.snapshot(WorkspacesDoc, conversation.id, context))?.items ?? [];
22877
+ if (!items.some((item) => item.path === path))
22878
+ throw new Error(WORKSPACE_UNKNOWN);
22879
+ if (items.length <= 1)
22880
+ throw new Error(WORKSPACE_LAST);
22881
+ for (const record of await scanConversations()) {
22882
+ if (record.owner)
22883
+ continue;
22884
+ if ((await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
22885
+ continue;
22886
+ const target = await harness.conversation(record.id, context);
22887
+ if (!target || savedWorkspace((await target.agent(context)).cwd) !== path)
22888
+ continue;
22889
+ const info = await harness.snapshot(SessionMetadataDoc, record.id, context);
22890
+ if (info?.status === "running" || info?.awaiting === true || info?.awaitingPlan === true)
22891
+ throw new Error(WORKSPACE_BUSY);
22892
+ }
22893
+ await harness.commit(async (tx) => {
22894
+ const doc = await tx.doc(WorkspacesDoc, conversation.id);
22895
+ const removed = await tx.doc(RemovedWorkspacesDoc, conversation.id);
22896
+ doc.items = doc.items.filter((item) => item.path !== path);
22897
+ if (!removed.paths.includes(path))
22898
+ removed.paths.push(path);
22899
+ }, context);
22900
+ };
22847
22901
  const jobIo = (id) => ({
22848
22902
  commit: (change) => harness.commit(change, context),
22849
22903
  state: () => harness.snapshot(JobsDoc, id, context)
@@ -23223,13 +23277,14 @@ async function createNekoRuntime(options = {}) {
23223
23277
  },
23224
23278
  touchConversation,
23225
23279
  addWorkspace,
23280
+ removeWorkspace,
23226
23281
  async listWorkspaces() {
23227
23282
  return [...(await harness.snapshot(WorkspacesDoc, conversation.id, context))?.items ?? []];
23228
23283
  },
23229
23284
  async forkConversation(id) {
23230
23285
  const source = await sessionFor(id);
23231
23286
  const agent = await source.conversation.agent(context);
23232
- await registerWorkspace(savedWorkspace(agent.cwd));
23287
+ await registerWorkspace(savedWorkspace(agent.cwd), false);
23233
23288
  const newest = await source.conversation.entries({}, 1, undefined, context);
23234
23289
  const at = newest.items[0]?.id;
23235
23290
  const sourceMetadata = at === undefined ? undefined : await harness.snapshot(SessionMetadataDoc, id, context);
@@ -23326,6 +23381,7 @@ async function createNekoRuntime(options = {}) {
23326
23381
  async listConversations() {
23327
23382
  const records = await scanConversations();
23328
23383
  const infos = [];
23384
+ const removed = await removedWorkspaces();
23329
23385
  for (const record of records) {
23330
23386
  if (record.owner)
23331
23387
  continue;
@@ -23334,6 +23390,9 @@ async function createNekoRuntime(options = {}) {
23334
23390
  const target = await harness.conversation(record.id, context);
23335
23391
  if (!target)
23336
23392
  continue;
23393
+ const workspaceRoot = savedWorkspace((await target.agent(context)).cwd);
23394
+ if (removed.has(workspaceRoot))
23395
+ continue;
23337
23396
  const info = await harness.snapshot(SessionMetadataDoc, record.id, context);
23338
23397
  infos.push({
23339
23398
  title: info.title,
@@ -23346,7 +23405,7 @@ async function createNekoRuntime(options = {}) {
23346
23405
  model: (await target.agent(context)).model ?? null,
23347
23406
  id: record.id,
23348
23407
  archived: false,
23349
- workspaceRoot: savedWorkspace((await target.agent(context)).cwd),
23408
+ workspaceRoot,
23350
23409
  lastUsedAt: (await harness.snapshot(ActivityDoc, record.id, context))?.lastUsedAt ?? 0
23351
23410
  });
23352
23411
  }
@@ -24075,13 +24134,14 @@ var page = Type8.Object({
24075
24134
  next: nullableString
24076
24135
  });
24077
24136
  var NekoWorkspaces = contract("neko.workspaces", {
24078
- version: 1,
24137
+ version: 2,
24079
24138
  input: Type8.Object({}, { additionalProperties: false }),
24080
24139
  variants: {
24081
24140
  ready: {
24082
24141
  state: { state: WorkspaceStateSchema },
24083
24142
  methods: {
24084
24143
  add: rpc(Type8.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
24144
+ remove: rpc(Type8.Object({ path: str }, { additionalProperties: false }), Type8.Null()),
24085
24145
  browse: rpc(browse, page)
24086
24146
  }
24087
24147
  }
@@ -24980,8 +25040,8 @@ function subagentClient(connect) {
24980
25040
  import { createHash as createHash2 } from "crypto";
24981
25041
 
24982
25042
  // packages/core/src/application-version.ts
24983
- var RUNNING_VERSION = "0.4.9";
24984
- var APPLICATION_PROTOCOL = 8;
25043
+ var RUNNING_VERSION = "0.5.0";
25044
+ var APPLICATION_PROTOCOL = 9;
24985
25045
  function stableVersion(value) {
24986
25046
  return typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value) && value.split(".").every((n) => Number.isSafeInteger(Number(n)));
24987
25047
  }
@@ -30078,6 +30138,7 @@ async function createNekoServerServices(options) {
30078
30138
  "app.neko.activity",
30079
30139
  "app.neko.conversation",
30080
30140
  "app.neko.workspaces",
30141
+ "app.neko.workspaces-removed",
30081
30142
  "pi.agent"
30082
30143
  ].includes(change.record.kind)))
30083
30144
  return;
@@ -30112,7 +30173,11 @@ async function createNekoServerServices(options) {
30112
30173
  variant: "ready",
30113
30174
  state: shared(self2).workspaces,
30114
30175
  browse: (input) => browseWorkspaces(input),
30115
- add: (input) => serialize(() => runtime.addWorkspace(input.path))
30176
+ add: (input) => serialize(() => runtime.addWorkspace(input.path)),
30177
+ remove: (input) => serialize(async () => {
30178
+ await runtime.removeWorkspace(input.path);
30179
+ return null;
30180
+ })
30116
30181
  }));
30117
30182
  host.serve(NekoCredentials, (_input, _principal, self2) => {
30118
30183
  const hub = shared(self2);
package/dist/neko.js CHANGED
@@ -80,7 +80,7 @@ function updateAction(input) {
80
80
  return "install";
81
81
  return "none";
82
82
  }
83
- var RUNNING_VERSION = "0.4.9", APPLICATION_PROTOCOL = 8, ApplicationProtocolError;
83
+ var RUNNING_VERSION = "0.5.0", APPLICATION_PROTOCOL = 9, ApplicationProtocolError;
84
84
  var init_application_version = __esm(() => {
85
85
  ApplicationProtocolError = class ApplicationProtocolError extends Error {
86
86
  constructor() {
@@ -805,9 +805,13 @@ import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
805
805
  class FileCredentialStore {
806
806
  path;
807
807
  #renameDeps;
808
- constructor(path = DEFAULT_CREDENTIALS_PATH, renameDeps = {}) {
808
+ #lockAttempts;
809
+ #lockRetryMs;
810
+ constructor(path = DEFAULT_CREDENTIALS_PATH, renameDeps = {}, lock = {}) {
809
811
  this.path = path;
810
812
  this.#renameDeps = renameDeps;
813
+ this.#lockAttempts = lock.lockAttempts ?? LOCK_ATTEMPTS;
814
+ this.#lockRetryMs = lock.lockRetryMs ?? LOCK_RETRY_MS;
811
815
  }
812
816
  async#read() {
813
817
  try {
@@ -843,7 +847,7 @@ class FileCredentialStore {
843
847
  lock = await open(lockPath, "wx", 384);
844
848
  break;
845
849
  } catch (error) {
846
- if (error.code !== "EEXIST" || attempt >= 100)
850
+ if (error.code !== "EEXIST" || attempt >= this.#lockAttempts)
847
851
  throw new Error("Credential storage is busy or unavailable.");
848
852
  if (!reclaimed) {
849
853
  const info = await stat2(lockPath).catch(() => {
@@ -855,7 +859,7 @@ class FileCredentialStore {
855
859
  continue;
856
860
  }
857
861
  }
858
- await new Promise((resolve) => setTimeout(resolve, 25));
862
+ await new Promise((resolve) => setTimeout(resolve, this.#lockRetryMs));
859
863
  }
860
864
  }
861
865
  const temporary = `${this.path}.${crypto.randomUUID()}.tmp`;
@@ -902,7 +906,7 @@ class FileCredentialStore {
902
906
  });
903
907
  }
904
908
  }
905
- var STALE_LOCK_MS = 30000, DEFAULT_CREDENTIALS_PATH;
909
+ var STALE_LOCK_MS = 30000, LOCK_ATTEMPTS = 100, LOCK_RETRY_MS = 25, DEFAULT_CREDENTIALS_PATH;
906
910
  var init_credentials = __esm(() => {
907
911
  init_rename_with_retry();
908
912
  init_credential_providers();
@@ -1025,6 +1029,7 @@ function accessibleWorkspace(directory) {
1025
1029
  }
1026
1030
  return path;
1027
1031
  }
1032
+ var WORKSPACE_UNKNOWN = "WORKSPACE_UNKNOWN: This workspace is not in the list.", WORKSPACE_LAST = "WORKSPACE_LAST: Keep at least one workspace.", WORKSPACE_BUSY = "WORKSPACE_BUSY: Stop or finish the running sessions of this workspace first.";
1028
1033
  var init_workspace = () => {};
1029
1034
 
1030
1035
  // packages/core/src/projection.ts
@@ -6634,20 +6639,57 @@ async function createNekoRuntime(options = {}) {
6634
6639
  contextState = resolveContextSettings(options.contextSettings, savedContext);
6635
6640
  return contextState;
6636
6641
  };
6637
- const registerWorkspace = async (path) => {
6642
+ const removedWorkspaces = async () => new Set((await harness.snapshot(RemovedWorkspacesDoc, conversation.id, context))?.paths ?? []);
6643
+ const registerWorkspace = async (path, explicit) => {
6638
6644
  await harness.commit(async (tx) => {
6639
6645
  const doc = await tx.doc(WorkspacesDoc, conversation.id);
6646
+ const removed = await tx.doc(RemovedWorkspacesDoc, conversation.id);
6647
+ if (removed.paths.includes(path)) {
6648
+ if (!explicit)
6649
+ return;
6650
+ removed.paths = removed.paths.filter((entry) => entry !== path);
6651
+ }
6640
6652
  if (!doc.items.some((item) => item.path === path))
6641
6653
  doc.items.push({ path, createdAt: Date.now() });
6642
6654
  }, context);
6655
+ };
6656
+ const addWorkspace = async (input) => {
6657
+ const path = accessibleWorkspace(input);
6658
+ await registerWorkspace(path, true);
6643
6659
  return (await harness.snapshot(WorkspacesDoc, conversation.id, context)).items.find((item) => item.path === path);
6644
6660
  };
6645
- const addWorkspace = async (path) => registerWorkspace(accessibleWorkspace(path));
6646
6661
  for (const record of [...existing, { id: conversation.id }]) {
6647
6662
  const target = await harness.conversation(record.id, context);
6648
6663
  if (target)
6649
- await registerWorkspace(savedWorkspace((await target.agent(context)).cwd));
6650
- }
6664
+ await registerWorkspace(savedWorkspace((await target.agent(context)).cwd), false);
6665
+ }
6666
+ await registerWorkspace(defaultCwd, true);
6667
+ const removeWorkspace = async (path) => {
6668
+ const items = (await harness.snapshot(WorkspacesDoc, conversation.id, context))?.items ?? [];
6669
+ if (!items.some((item) => item.path === path))
6670
+ throw new Error(WORKSPACE_UNKNOWN);
6671
+ if (items.length <= 1)
6672
+ throw new Error(WORKSPACE_LAST);
6673
+ for (const record of await scanConversations()) {
6674
+ if (record.owner)
6675
+ continue;
6676
+ if ((await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
6677
+ continue;
6678
+ const target = await harness.conversation(record.id, context);
6679
+ if (!target || savedWorkspace((await target.agent(context)).cwd) !== path)
6680
+ continue;
6681
+ const info = await harness.snapshot(SessionMetadataDoc, record.id, context);
6682
+ if (info?.status === "running" || info?.awaiting === true || info?.awaitingPlan === true)
6683
+ throw new Error(WORKSPACE_BUSY);
6684
+ }
6685
+ await harness.commit(async (tx) => {
6686
+ const doc = await tx.doc(WorkspacesDoc, conversation.id);
6687
+ const removed = await tx.doc(RemovedWorkspacesDoc, conversation.id);
6688
+ doc.items = doc.items.filter((item) => item.path !== path);
6689
+ if (!removed.paths.includes(path))
6690
+ removed.paths.push(path);
6691
+ }, context);
6692
+ };
6651
6693
  const jobIo = (id) => ({
6652
6694
  commit: (change) => harness.commit(change, context),
6653
6695
  state: () => harness.snapshot(JobsDoc, id, context)
@@ -7027,13 +7069,14 @@ async function createNekoRuntime(options = {}) {
7027
7069
  },
7028
7070
  touchConversation,
7029
7071
  addWorkspace,
7072
+ removeWorkspace,
7030
7073
  async listWorkspaces() {
7031
7074
  return [...(await harness.snapshot(WorkspacesDoc, conversation.id, context))?.items ?? []];
7032
7075
  },
7033
7076
  async forkConversation(id) {
7034
7077
  const source = await sessionFor(id);
7035
7078
  const agent = await source.conversation.agent(context);
7036
- await registerWorkspace(savedWorkspace(agent.cwd));
7079
+ await registerWorkspace(savedWorkspace(agent.cwd), false);
7037
7080
  const newest = await source.conversation.entries({}, 1, undefined, context);
7038
7081
  const at = newest.items[0]?.id;
7039
7082
  const sourceMetadata = at === undefined ? undefined : await harness.snapshot(SessionMetadataDoc, id, context);
@@ -7130,6 +7173,7 @@ async function createNekoRuntime(options = {}) {
7130
7173
  async listConversations() {
7131
7174
  const records = await scanConversations();
7132
7175
  const infos = [];
7176
+ const removed = await removedWorkspaces();
7133
7177
  for (const record of records) {
7134
7178
  if (record.owner)
7135
7179
  continue;
@@ -7138,6 +7182,9 @@ async function createNekoRuntime(options = {}) {
7138
7182
  const target = await harness.conversation(record.id, context);
7139
7183
  if (!target)
7140
7184
  continue;
7185
+ const workspaceRoot = savedWorkspace((await target.agent(context)).cwd);
7186
+ if (removed.has(workspaceRoot))
7187
+ continue;
7141
7188
  const info = await harness.snapshot(SessionMetadataDoc, record.id, context);
7142
7189
  infos.push({
7143
7190
  title: info.title,
@@ -7150,7 +7197,7 @@ async function createNekoRuntime(options = {}) {
7150
7197
  model: (await target.agent(context)).model ?? null,
7151
7198
  id: record.id,
7152
7199
  archived: false,
7153
- workspaceRoot: savedWorkspace((await target.agent(context)).cwd),
7200
+ workspaceRoot,
7154
7201
  lastUsedAt: (await harness.snapshot(ActivityDoc, record.id, context))?.lastUsedAt ?? 0
7155
7202
  });
7156
7203
  }
@@ -7347,7 +7394,7 @@ async function createNekoRuntime(options = {}) {
7347
7394
  }
7348
7395
  };
7349
7396
  }
7350
- var DEFAULT_MODEL, DEFAULT_STORAGE_PATH, neverAborted = () => new AbortController().signal, ArchivedDoc, WorkspacesDoc, SearchSettingsDoc, ProviderSettingsDoc, DefaultModelDoc, ContextSettingsDoc, ActivityDoc;
7397
+ var DEFAULT_MODEL, DEFAULT_STORAGE_PATH, neverAborted = () => new AbortController().signal, ArchivedDoc, WorkspacesDoc, RemovedWorkspacesDoc, SearchSettingsDoc, ProviderSettingsDoc, DefaultModelDoc, ContextSettingsDoc, ActivityDoc;
7351
7398
  var init_runtime = __esm(() => {
7352
7399
  init_agent_file_search();
7353
7400
  init_compaction();
@@ -7399,6 +7446,14 @@ var init_runtime = __esm(() => {
7399
7446
  fork: "initial",
7400
7447
  initial: () => ({ items: [] })
7401
7448
  });
7449
+ RemovedWorkspacesDoc = defineDoc8({
7450
+ kind: "app.neko.workspaces-removed",
7451
+ version: 1,
7452
+ scope: "conversation",
7453
+ history: "latest",
7454
+ fork: "initial",
7455
+ initial: () => ({ paths: [] })
7456
+ });
7402
7457
  SearchSettingsDoc = defineDoc8({
7403
7458
  kind: "app.neko.search-settings",
7404
7459
  version: 1,
@@ -8012,13 +8067,14 @@ var init_contract = __esm(() => {
8012
8067
  next: nullableString
8013
8068
  });
8014
8069
  NekoWorkspaces = contract("neko.workspaces", {
8015
- version: 1,
8070
+ version: 2,
8016
8071
  input: Type8.Object({}, { additionalProperties: false }),
8017
8072
  variants: {
8018
8073
  ready: {
8019
8074
  state: { state: WorkspaceStateSchema },
8020
8075
  methods: {
8021
8076
  add: rpc(Type8.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
8077
+ remove: rpc(Type8.Object({ path: str }, { additionalProperties: false }), Type8.Null()),
8022
8078
  browse: rpc(browse, page)
8023
8079
  }
8024
8080
  }
@@ -30954,6 +31010,7 @@ async function createNekoServerServices(options) {
30954
31010
  "app.neko.activity",
30955
31011
  "app.neko.conversation",
30956
31012
  "app.neko.workspaces",
31013
+ "app.neko.workspaces-removed",
30957
31014
  "pi.agent"
30958
31015
  ].includes(change.record.kind)))
30959
31016
  return;
@@ -30988,7 +31045,11 @@ async function createNekoServerServices(options) {
30988
31045
  variant: "ready",
30989
31046
  state: shared(self2).workspaces,
30990
31047
  browse: (input) => browseWorkspaces(input),
30991
- add: (input) => serialize(() => runtime.addWorkspace(input.path))
31048
+ add: (input) => serialize(() => runtime.addWorkspace(input.path)),
31049
+ remove: (input) => serialize(async () => {
31050
+ await runtime.removeWorkspace(input.path);
31051
+ return null;
31052
+ })
30992
31053
  }));
30993
31054
  host.serve(NekoCredentials, (_input, _principal, self2) => {
30994
31055
  const hub = shared(self2);
package/package.json CHANGED
@@ -11,7 +11,8 @@
11
11
  "build": "bun run --cwd packages/core build && bun run --cwd packages/gateway build && bun run --cwd packages/tui build && bun run --cwd packages/web build && bun run --cwd packages/cli build",
12
12
  "check": "bun --bun run biome check . && bun --bun run tsc --noEmit && bun run --cwd packages/web check:client",
13
13
  "check:write": "bun --bun run biome check --write . && bun --bun run tsc --noEmit && bun run --cwd packages/web check:client",
14
- "test": "bun test --conditions=neko-source --isolate",
14
+ "test": "bun test --conditions=neko-source --parallel",
15
+ "test:serial": "bun test --conditions=neko-source --isolate",
15
16
  "neko": "bun --conditions=neko-source packages/cli/src/main.ts",
16
17
  "build:release": "bun run build && bun scripts/build-release.ts",
17
18
  "test:package": "bun scripts/test-package.ts"
@@ -25,7 +26,7 @@
25
26
  },
26
27
  "packageManager": "bun@1.4.2",
27
28
  "trustedDependencies": [],
28
- "version": "0.4.9",
29
+ "version": "0.5.0",
29
30
  "description": "A local AI coding assistant with terminal and web interfaces, powered by Pi and Bun.",
30
31
  "keywords": [
31
32
  "ai",